1. 程式人生 > >視頻播放技術匯總(列表播放,小窗播放,跨界面播放,播放中網絡切換提示)

視頻播放技術匯總(列表播放,小窗播放,跨界面播放,播放中網絡切換提示)

lean 註意 art 序言 text owin getparent 自己 remove

序言

近期的項目中涉及到視頻播放。在這裏我把關於視頻播放技術中的一些心得體會記錄下來。

功能

完整演示

技術分享圖片

安裝地址

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

視頻播放技術匯總(列表播放,小窗播放,跨界面播放,播放中網絡切換提示)