视频播放技术汇总(列表播放,小窗播放,跨界面播放,播放中网络切换提示)

来源:互联网 发布:数据库的数据模型包含 编辑:程序博客网 时间:2024/06/07 16:21

序言

最近的项目中涉及到视频播放,在这里我把关于视频播放技术中的一些心得体会记录下来。

功能

完整演示

这里写图片描述

安装地址

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

2 1
原创粉丝点击