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

序言

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

功能

完整演示

這里寫圖片描述

安裝地址

http://pre.im/lNm8

這里寫圖片描述

基本功能

1.在無wifi的情況下提示用戶,包括正在播放的時候網絡切換也會提示用戶。

這里寫圖片描述

2.小窗播放:當用戶正在觀看的視頻沒有播完,用戶又滑動到其他頁面則視頻繼續(xù)在小窗播放,播放完成以后小窗自動消失,并提示用戶播放完畢。

這里寫圖片描述

播放完畢提示

這里寫圖片描述

3.列表播放:支持在列表中播放

這里寫圖片描述

4.跨界面播放,在列表中播放時,點擊列表進入詳情頁?;蛟谛〈安シ艜r點擊小窗進入詳情頁。視頻將繼續(xù)播放,不會重頭開始。

實現(xiàn)

關于視頻在任意位置播放,我主要是通過一個VideoPlayManager來管理的。在VideoPlayManager中有一個用來播放視頻的VideoPlayView,而在需要播放視頻的時候通過Rxbus發(fā)送一個事件,事件包含了能夠展示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發(fā)送事件來播放和切換播放容器
 * 在程序運行期間通過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;

    //用于播放完成的監(jiān)聽器
    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();
        //創(chuàng)建小窗播放容器
        createSmallWindow();
        //注冊事件 處理
        registerEvent();
        Application application = (Application) sContext;
        //監(jiān)聽應用前后臺的切換
        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;
    };

    /**
     * 用于顯示視頻的線程
     * 在應用進入前臺的時候啟動,在切換到后臺的時候停止
     * 負責,判斷當前的顯示狀態(tài)并顯示到正確位置
     */
    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;
        //實現(xiàn)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;
                    //實現(xiàn)點擊事件
                    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;
    }


    /**
     * 用于監(jiān)控應用前后臺的切換
     */
    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);
                }
                //繼續(xù)播放視頻
                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());


            //重置狀態(tài),保存播放的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中每次
                //創(chuàng)建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中會有移除屏幕的情況發(fā)生
                    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;
    }


    /**
     * 取消播放事件,比如應用程序退出時發(fā)出這個時間
     */
    public static class PlayCancleEvent {
    }

    /**
     * 視頻播放退出
     */
    public static class PlayVideoBackEvent {
    }

    /**
     * 將視頻顯示在指定的View中
     * 如果視頻發(fā)生改變則播放視頻
     * 如果view發(fā)生改變但是視頻沒有改變,則只是切換播放的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;
        }
    }
}

視頻播放的時候只需要發(fā)送一個消息就行了。

   RxBus.getDefault().post(new VideoPlayManager.PlayInViewEvent(holder.layout_holder, videoItem, true));

需要注意的時候,為了能在ListView和RecyclerView中播放,需要將播放的item綁定的播放容器上,這樣在線程檢測當前界面是否有能播放視頻的容器時才不會因為RecyclerView的復用而出錯。

     holder.layout_holder.setTag(videoItem);

關于更多的細節(jié)大家看我的Demo吧,內容實在太多。

Demo

https://github.com/zhuguohui/VideoDemo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評論 25 708
  • 最近做了一個Android UI相關開源項目庫匯總,里面集合了OpenDigg 上的優(yōu)質的Android開源項目庫...
    OpenDigg閱讀 17,628評論 6 222
  • 真的是忙于交際,沒有太多精力學習。連寫總結的欲望都沒有,今天學了朱偉的戀戀有詞,明天有機會繼續(xù)吧。明天應該早點起,...
    muziyue閱讀 272評論 0 0
  • 什么是區(qū)塊鏈? 區(qū)塊鏈的英文是Block Chain,由Block和Chain組成,顧名思義,它應該包含兩方面的意...
    楊何閱讀 1,198評論 0 0
  • 今天特地選了一個自己完全不了解的領域書籍:行為心理學讀起來特別燒腦,他們把"人"稱為"有機體"或"個體",各種行為...
    品興閱讀 297評論 0 0

友情鏈接更多精彩內容