視頻播放技術匯總(列表播放,小窗播放,跨界面播放,播放中網絡切換提示)
序言
近期的項目中涉及到視頻播放。在這裏我把關於視頻播放技術中的一些心得體會記錄下來。
功能
完整演示
安裝地址
http://pre.im/lNm8
基本功能
1.在無wifi的情況下提示用戶。包括正在播放的時候網絡切換也會提示用戶。
2.小窗播放:當用戶正在觀看的視頻沒有播完,用戶又滑動到其它頁面則視頻繼續在小窗播放,播放完畢以後小窗自己主動消失。並提示用戶播放完畢。
播放完畢提示
3.列表播放:支持在列表中播放
4.跨界面播放。在列表中播放時。點擊列表進入詳情頁。或在小窗播放時點擊小窗進入詳情頁。視頻將繼續播放,不會重頭開始。
實現
關於視頻在任何位置播放,我主要是通過一個VideoPlayManager來管理的。在VideoPlayManager中有一個用來播放視頻的VideoPlayView。而在須要播放視頻的時候通過Rxbus發送一個事件,事件包括了可以展示VideoPlayView的FragmeLayout和須要播放的視頻資源。VideoPlayManager初始化的時候開啟了一個線程用來檢測當前視頻須要播放的位置。
package com.zhuguohui.videodemo.video;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.trs.videolist.CustomMediaContoller;
import com.trs.videolist.VideoPlayView;
import com.zhuguohui.videodemo.R;
import com.zhuguohui.videodemo.activity.FullscreenActivity;
import com.zhuguohui.videodemo.adapter.VideoAdapter;
import com.zhuguohui.videodemo.bean.VideoItem;
import com.zhuguohui.videodemo.rx.RxBus;
import com.zhuguohui.videodemo.service.NetworkStateService;
import com.zhuguohui.videodemo.util.AppUtil;
import com.zhuguohui.videodemo.util.ToastUtil;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
/**
* 用於管理視頻播放的工具類
* <p>
* 通過RxBus發送事件來播放和切換播放容器
* 在程序執行期間通過displayThread自己主動在小窗模式,列表模式切換。
* <p>
* Created by zhuguohui on 2017/1/11 0011.
*/
public class VideoPlayManager {
private static WindowManager windowManager;
private static Context sContext;
private static boolean haveInit = false;
//小窗播放
private static FrameLayout smallPlayHolder;
private static RelativeLayout smallWindow;
private static LayoutParams smallWindowParams;
//小窗關閉的button
private static ImageView iv_close;
private static VideoPlayView sVideoPlayView;
//正在播放的Item
private static VideoItem sPlayingItem = null;
//正在臨時視頻的容器
private static ViewGroup sPlayingHolder = null;
//當前的Activity
private static Activity currentActivity;
//標識是否在後臺執行
private static boolean runOnBack = false;
//用於播放完畢的監聽器
private static CompletionListener completionListener = new CompletionListener();
//標識是否在小窗模式
private static boolean sPlayInSmallWindowMode = false;
//用於在主線程中更新UI
private static Handler handler = new Handler(Looper.getMainLooper());
//記錄在小窗中按下的位置
private static float xDownInSmallWindow, yDownInSmallWindow;
//記錄在小窗中上一次觸摸的位置
private static float lastX, lastY = 0;
private static VideoAdapter.VideoClickListener videoClickListener = new VideoAdapter.VideoClickListener();
public static void init(Context context) {
if (haveInit) {
return;
}
sContext = context.getApplicationContext();
windowManager = (WindowManager) sContext.getSystemService(Context.WINDOW_SERVICE);
//初始化播放容器
initVideoPlayView();
//創建小窗播放容器
createSmallWindow();
//註冊事件 處理
registerEvent();
Application application = (Application) sContext;
//監聽應用前後臺的切換
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
haveInit = true;
}
/**
* 初始化播放控件
*/
private static void initVideoPlayView() {
sVideoPlayView = new VideoPlayView(sContext);
sVideoPlayView.setCompletionListener(completionListener);
sVideoPlayView.setFullScreenChangeListener(fullScreenChangeListener);
sVideoPlayView.setOnErrorListener(onErrorListener);
}
private static IMediaPlayer.OnErrorListener onErrorListener = (mp, what, extra) -> {
ToastUtil.getInstance().showToast("播放失敗");
completionListener.completion(null);
return true;
};
/**
* 用於顯示視頻的線程
* 在應用進入前臺的時候啟動,在切換到後臺的時候停止
* 負責,推斷當前的顯示狀態並顯示到正確位置
*/
private static void createSmallWindow() {
smallWindow = (RelativeLayout) View.inflate(sContext, R.layout.view_small_holder, null);
smallPlayHolder = (FrameLayout) smallWindow.findViewById(R.id.small_holder);
//關閉button
iv_close = (ImageView) smallWindow.findViewById(R.id.iv_close);
iv_close.setOnClickListener(v ->
{
if (sVideoPlayView.isPlay()) {
sVideoPlayView.stop();
sVideoPlayView.release();
}
completionListener.completion(null);
});
smallWindowParams = new LayoutParams();
int width = AppUtil.dip2px(sContext, 160);
int height = AppUtil.dip2px(sContext, 90);
smallWindowParams.width = width;
smallWindowParams.height = height;
smallWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
smallWindowParams.x = 0;
smallWindowParams.y = 0;
/* if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
smallWindowParams.type = LayoutParams.TYPE_TOAST;
} else {
smallWindowParams.type = LayoutParams.TYPE_PHONE;
}*/
smallWindowParams.type = LayoutParams.TYPE_SYSTEM_ERROR;
smallWindowParams.flags = FLAG_NOT_FOCUSABLE | FLAG_KEEP_SCREEN_ON;
// 設置期望的bitmap格式
smallWindowParams.format = PixelFormat.RGBA_8888;
//實現view可拖動
smallWindow.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case ACTION_DOWN:
xDownInSmallWindow = event.getRawX();
yDownInSmallWindow = event.getRawY();
lastX = xDownInSmallWindow;
lastY = yDownInSmallWindow;
break;
case ACTION_MOVE:
float moveX = event.getRawX() - lastX;
float moveY = event.getRawY() - lastY;
lastX = event.getRawX();
lastY = event.getRawY();
if (Math.abs(moveX) > 10 || Math.abs(moveY) > 10) {
//更新
smallWindowParams.x += moveX;
smallWindowParams.y += moveY;
windowManager.updateViewLayout(smallWindow, smallWindowParams);
return true;
}
break;
case ACTION_UP:
moveX = event.getRawX() - xDownInSmallWindow;
moveY = event.getRawY() - yDownInSmallWindow;
//實現點擊事件
if (Math.abs(moveX) < 10 && Math.abs(moveY) < 10) {
videoClickListener.onVideoClick(currentActivity, sPlayingItem);
return true;
}
break;
}
return false;
});
}
/**
* 請求用戶給予懸浮窗的權限
*/
public static boolean askForPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(currentActivity)) {
// Toast.makeText(TestFloatWinActivity.this, "當前無權限。請授權!", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + currentActivity.getPackageName()));
// currentActivity.startActivityForResult(intent,OVERLAY_PERMISSION_REQ_CODE);
currentActivity.startActivity(intent);
return false;
} else {
return true;
}
}
return true;
}
/**
* 用於監控應用前後臺的切換
*/
private static Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
private int count = 0;
private boolean videoPause = false;
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
if (count == 0) {
//切換到前臺
runOnBack = false;
if (sPlayInSmallWindowMode) {
windowManager.addView(smallWindow, smallWindowParams);
}
//繼續播放視頻
if (videoPause) {
sVideoPlayView.pause();
videoPause = false;
}
DisPlayThread.startDisplay();
}
count++;
}
@Override
public void onActivityResumed(Activity activity) {
currentActivity = activity;
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
count--;
if (count == 0) {
//切換到後臺
runOnBack = true;
//停止檢測線程
DisPlayThread.stopDisplay();
//假設是小窗模式移除window
if (sPlayInSmallWindowMode) {
windowManager.removeView(smallWindow);
}
//視頻暫停
if (sVideoPlayView.isPlay()) {
sVideoPlayView.pause();
videoPause = true;
}
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
};
/**
* 退出全屏
*/
private static void exitFromFullScreenMode() {
currentActivity.finish();
}
private static CustomMediaContoller.FullScreenChangeListener fullScreenChangeListener = () -> {
if (!(currentActivity instanceof FullscreenActivity)) {
enterFullScreenMode();
} else {
exitFromFullScreenMode();
}
};
private static void enterFullScreenMode() {
currentActivity.startActivity(new Intent(currentActivity, FullscreenActivity.class));
}
private static class CompletionListener implements VideoPlayView.CompletionListener {
@Override
public void completion(IMediaPlayer mp) {
if (currentActivity instanceof FullscreenActivity) {
currentActivity.finish();
}
//假設是小窗播放則退出小窗
if (sPlayInSmallWindowMode) {
if (mp != null) {
//mp不等於null表示正常的播放完畢退出
//在小窗消失之前給用戶一個提示消息,防止太突兀
ToastUtil.getInstance().ok().showToast("播放完畢");
}
exitFromSmallWindowMode();
}
//將播放控件從器父View中移出
removeVideoPlayViewFromParent();
sPlayingItem = null;
if (sPlayingHolder != null) {
sPlayingHolder.setKeepScreenOn(false);
}
sPlayingHolder = null;
//釋放資源
sVideoPlayView.release();
}
}
/**
* 註冊事件處理
*/
private static void registerEvent() {
//處理在View中播放
RxBus.getDefault().toObserverable(PlayInViewEvent.class).subscribe(playInViewEvent -> {
//表示播放容器,和視頻內容是否變化
boolean layoutChange = sPlayingHolder == null || !sPlayingHolder.equals(playInViewEvent.getPlayLayout());
boolean videoChange = sPlayingItem == null || !sPlayingItem.equals(playInViewEvent.getNewsItem());
//重置狀態,保存播放的Holder
if (videoChange) {
sPlayingItem = playInViewEvent.getNewsItem();
}
if (layoutChange) {
removeVideoPlayViewFromParent();
if (sPlayingHolder != null) {
//關閉之前View的屏幕常亮
sPlayingHolder.setKeepScreenOn(false);
}
sPlayingHolder = playInViewEvent.getPlayLayout();
//將播放的Item設置為播放view的tag,就行通過displayThread檢查當前Activity中是否
//包括了這個tag的View存在,而直到是否有播放容器存在,假設沒有的話就使用小窗播放。
sPlayingHolder.setTag(sPlayingItem);
//顯示控制條
sVideoPlayView.setShowContoller(true);
//開啟屏幕常亮
sVideoPlayView.setKeepScreenOn(true);
sPlayingHolder.addView(sVideoPlayView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
if (videoChange) {
//播放新視頻
if (sVideoPlayView.isPlay()) {
sVideoPlayView.stop();
sVideoPlayView.release();
}
sPlayingHolder.setTag(sPlayingItem);
//推斷網絡,假設在移動網絡則提示用戶
ViedoPlayChecker.checkPlayNet(currentActivity, () -> {
sVideoPlayView.start(sPlayingItem.getVideoUrl());
}, () -> {
completionListener.completion(null);
});
} else {
//重播
if (!sVideoPlayView.isPlay()) {
sVideoPlayView.start(sPlayingItem.getVideoUrl());
}
}
});
//處理視頻回退
RxBus.getDefault().toObserverable(PlayVideoBackEvent.class).subscribe(playVideoBackEvent -> {
sPlayingHolder = null;
});
//處理網絡變化
RxBus.getDefault().toObserverable(NetworkStateService.NetStateChangeEvent.class).subscribe(netStateChangeEvent -> {
if (netStateChangeEvent.getState() == NetworkStateService.NetStateChangeEvent.NetState.NET_4G && sVideoPlayView.isPlay()) {
sVideoPlayView.pause();
//假設在移動網絡播放,則提示用戶
ViedoPlayChecker.checkPlayNet(currentActivity, () -> {
sVideoPlayView.pause();
}, () -> {
completionListener.completion(null);
});
}
});
//處理取消播放事件
RxBus.getDefault().toObserverable(PlayCancleEvent.class).subscribe(playCancleEvent -> {
completionListener.completion(null);
});
}
/**
* 進入小窗播放模式
*/
private static void enterSmallWindowMode() {
//檢查權限
if (!askForPermission()) {
ToastUtil.getInstance().showToast("小窗播放須要浮窗權限");
return;
}
if (!sPlayInSmallWindowMode) {
handler.post(() -> {
removeVideoPlayViewFromParent();
//隱藏控制條
sVideoPlayView.setShowContoller(false);
smallPlayHolder.addView(sVideoPlayView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
try {
windowManager.addView(smallWindow, smallWindowParams);
} catch (Exception e) {
e.printStackTrace();
//已經加入了。則更新
windowManager.updateViewLayout(smallWindow, smallWindowParams);
}
sPlayingHolder = smallPlayHolder;
sPlayInSmallWindowMode = true;
});
}
}
/**
* 退出小窗播放模式
*/
private static void exitFromSmallWindowMode() {
if (sPlayInSmallWindowMode) {
handler.post(() -> {
windowManager.removeView(smallWindow);
sPlayInSmallWindowMode = false;
//顯示控制條
sVideoPlayView.setShowContoller(true);
});
}
}
private static void removeVideoPlayViewFromParent() {
if (sVideoPlayView != null) {
if (sVideoPlayView.getParent() != null) {
ViewGroup parent = (ViewGroup) sVideoPlayView.getParent();
parent.removeView(sVideoPlayView);
}
}
}
public static class DisPlayThread extends Thread {
private boolean check = false;
private static DisPlayThread disPlayThread;
public synchronized static void startDisplay() {
if (disPlayThread != null) {
stopDisplay();
}
disPlayThread = new DisPlayThread();
disPlayThread.start();
}
public synchronized static void stopDisplay() {
if (disPlayThread != null) {
disPlayThread.cancel();
disPlayThread = null;
}
}
private void cancel() {
check = false;
}
private DisPlayThread() {
}
@Override
public void run() {
while (check) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//假設在後臺執行,直接退出
if (runOnBack) {
check = false;
stopDisplay();
return;
}
//檢查是否有正在播放的Item,假設沒有則不顯示不論什麽播放界面
if (sPlayingItem == null) {
continue;
}
//檢查是否有可播放的容器,通過Tag查找,不能通過id查找
//由於在ListView或者RecycleView中View是會復用的,因此須要在ListView,或RecycleView中每次
//創建holder的時候把tag設置到須要展示Video的FrameLayout上。
//使用正在播放的item作為tag;
if (currentActivity != null) {
View contentView = currentActivity.findViewById(android.R.id.content);
View playView = contentView.findViewWithTag(sPlayingItem);
//推斷正在播放的view是否是顯示在界面的,在ListView或RecycleView中會有移除屏幕的情況發生
if (isShowInWindow(playView)) {
//假設顯示,推斷是否和之前顯示的是否是同一個View
//假設不是則切換到當前view中
exitFromSmallWindowMode();
if (sPlayingHolder != playView) {
handler.post(() -> {
//關閉屏幕常亮
if (sPlayingHolder != null) {
sPlayingHolder.setKeepScreenOn(false);
}
removeVideoPlayViewFromParent();
ViewGroup viewGroup = (ViewGroup) playView;
viewGroup.addView(sVideoPlayView, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
sPlayingHolder = viewGroup;
//保持屏幕常亮
sPlayingHolder.setKeepScreenOn(true);
});
}
} else {
//假設不顯示。則在小窗中播放
enterSmallWindowMode();
}
}
}
}
Rect r = new Rect();
private boolean isShowInWindow(View view) {
if (view == null) {
return false;
}
boolean localVisibleRect = view.getLocalVisibleRect(r);
boolean show = localVisibleRect && view.isShown();
return show;
}
@Override
public synchronized void start() {
check = true;
super.start();
}
}
public static VideoItem getPlayingItem() {
return sPlayingItem;
}
/**
* 取消播放事件,比方應用程序退出時發出這個時間
*/
public static class PlayCancleEvent {
}
/**
* 視頻播放退出
*/
public static class PlayVideoBackEvent {
}
/**
* 將視頻顯示在指定的View中
* 假設視頻發生改變則播放視頻
* 假設view發生改變可是視頻沒有改變,則僅僅是切換播放的view。
*/
public static class PlayInViewEvent {
FrameLayout playLayout;
VideoItem newsItem;
boolean playInList;
public PlayInViewEvent(FrameLayout playLayout, VideoItem newsItem) {
this(playLayout, newsItem, false);
}
public PlayInViewEvent(FrameLayout playLayout, VideoItem newsItem, boolean playInList) {
this.playLayout = playLayout;
this.newsItem = newsItem;
this.playInList = playInList;
}
public VideoItem getNewsItem() {
return newsItem;
}
public void setNewsItem(VideoItem newsItem) {
this.newsItem = newsItem;
}
public FrameLayout getPlayLayout() {
return playLayout;
}
public void setPlayLayout(FrameLayout playLayout) {
this.playLayout = playLayout;
}
}
}
視頻播放的時候僅僅須要發送一個消息就行了。
RxBus.getDefault().post(new VideoPlayManager.PlayInViewEvent(holder.layout_holder, videoItem, true));
須要註意的時候。為了能在ListView和RecyclerView中播放,須要將播放的item綁定的播放容器上,這樣在線程檢測當前界面是否有能播放視頻的容器時才不會由於RecyclerView的復用而出錯。
holder.layout_holder.setTag(videoItem);
關於很多其它的細節大家看我的Demo吧,內容實在太多。
Demo
https://github.com/zhuguohui/VideoDemo
視頻播放技術匯總(列表播放,小窗播放,跨界面播放,播放中網絡切換提示)