如何写一个播放器-解析MNVideoPlayer(二)

本文适合初学者,详细解读了一个Android播放器项目中的事件监听(使用观察者模式)和网络变化监听(动态广播)。讨论了如何处理网络变化时的播放控制,如自动播放、缓存策略等,并预告了下篇将解析视频播放、缓存和预加载相关内容。
摘要由CSDN通过智能技术生成

注:本文适合初学Android或未接触过系统自带的MediaPlayer人群,阅读之前请下载相关代码

MNVideoPlayer代码:http://blog.csdn.net/wenqiang0718/article/details/78615715

由于此项目代码结构非常清晰,所以我们这次采用一个与众不同的方式进行解读,从下开始,之后从上开始,最终核心视频播放及销毁的方式进行代码解析。其实我们很多时候也需要这样,因为如果接手了别人的代码之后,并不会所有的代码都是从开始- ->结束,这样的顺序来让我们完整的吸收,而是遇到问题-->找到问题所在-->分析问题原因-->找到解决方式-->分析解决影响-->解决问题,总是这样一个顺序去解决已有工程的bug,包括我们自己写出来的bug也一样,短时间内还好,时间长了可能也只记得一个大概了。好了,废话不多说,下面我们进行demo代码解析:


最下面是事件监听的定义,很明显我们事件的监听使用的观察者模式:

//网络监听回调
    private OnNetChangeListener onNetChangeListener;

    public void setOnNetChangeListener(OnNetChangeListener onNetChangeListener) {
        this.onNetChangeListener = onNetChangeListener;
    }

    public interface OnNetChangeListener {
        //wifi
        void onWifi(MediaPlayer mediaPlayer);

        //手机
        void onMobile(MediaPlayer mediaPlayer);

        //不可用
        void onNoAvailable(MediaPlayer mediaPlayer);
    }

    //SurfaceView初始化完成回调
    private OnPlayerCreatedListener onPlayerCreatedListener;

    public void setOnPlayerCreatedListener(OnPlayerCreatedListener onPlayerCreatedListener) {
        this.onPlayerCreatedListener = onPlayerCreatedListener;
    }

    public interface OnPlayerCreatedListener {
        //不可用
        void onPlayerCreated(String url, String title);
    }

    //-----------------------播放完回调
    private OnCompletionListener onCompletionListener;

    public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
        this.onCompletionListener = onCompletionListener;
    }

    public interface OnCompletionListener {
        void onCompletion(MediaPlayer mediaPlayer);
    }

这些事件监听很简单,一个接口定义,一个set方法,在使用的时候判定常量是否为null,如果不为null则触发相关方法,几乎所有的观察者模式都是这样,非常简单,这也是观察者的魅力所在。

那么有设置的地方,最好就有销毁的地方,定义一个销毁所有监听的方法(这个是很有必要的):

private void removeAllListener() {
        if (onNetChangeListener != null) {
            onNetChangeListener = null;
        }
        if (onPlayerCreatedListener != null) {
            onPlayerCreatedListener = null;
        }
    }

因为demo中就设置了这两个方法,所以作者也就在remove中写了这两个方法,我们自己可以适量的增加。如果使用观察者的地方比较多,那么我建议使用集合来保存所有的监听,例如:

List<OnNetChangeListener> onNetChangeListenerList = new ArrayList<>();
    
    public void registerNetChangeListener(OnNetChangeListener onNetChangeListener){
        synchronized (onNetChangeListenerList){
            if(!onNetChangeListenerList.contains(onNetChangeListener)){
                onNetChangeListenerList.add(onNetChangeListener);
            }
        }
    }
    
    public void unRegisterNetChangeListener(OnNetChangeListener onNetChangeListener){
        synchronized (onNetChangeListenerList){
            if(onNetChangeListenerList.contains(onNetChangeListener)){
                onNetChangeListenerList.remove(onNetChangeListener);
            }
        }
    }
    
    public void clearNetChangeListener(){
        onNetChangeListenerList.clear();
    }

之所以使用 Synchronized关键字,是防止同时调用

接下来是网络变化监听,采用的动态广播的方式,优点是灵活,但是不要忘记注销就可以,而且我们可以在网络监听的时候DIY自己的功能,例如重新恢复网络时自动播放,网络切换为4G时弹窗提醒,网络断开后如果还有缓存则不会立即停止视频播放等,根据需求灵活编写即可:

//-------------------------网络变化监听
    public class NetChangeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (onNetChangeListener == null || !isNeedNetChangeListen) {
                return;
            }
            ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
            if (netInfo != null && netInfo.isAvailable()) {
                if (netInfo.getType() == ConnectivityManager.TYPE_WIFI) { //WiFi网络
                    onNetChangeListener.onWifi(mediaPlayer);
                } else if (netInfo.getType() == ConnectivityManager.TYPE_MOBILE) {   //3g网络
                    onNetChangeListener.onMobile(mediaPlayer);
                } else {    //其他
                    Log.i(TAG, "其他网络");
                }
            } else {
                onNetChangeListener.onNoAvailable(mediaPlayer);
            }
        }
    }

    private NetChangeReceiver netChangeReceiver;

    private void registerNetReceiver() {
        if (netChangeReceiver == null) {
            IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
            netChangeReceiver = new NetChangeReceiver();
            context.registerReceiver(netChangeReceiver, filter);
        }
    }

    private void unregisterNetReceiver() {
        if (netChangeReceiver != null) {
            context.unregisterReceiver(netChangeReceiver);
        }
    }

之后是电量监听,同样是动态广播监听:

/**
     * 电量广播接受者
     */
    class BatteryReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //判断它是否是为电量变化的Broadcast Action
            if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
                //获取当前电量
                int level = intent.getIntExtra("level", 0);
                //电量的总刻度
                int scale = intent.getIntExtra("scale", 100);

                int battery = (level * 100) / scale;

                //把它转成百分比
                Log.i(TAG, "电池电量为" + battery + "%");

                mn_iv_battery.setVisibility(View.VISIBLE);
                if (battery > 0 && battery < 20) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_01);
                } else if (battery >= 20 && battery < 40) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_02);
                } else if (battery >= 40 && battery < 65) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_03);
                } else if (battery >= 65 && battery < 90) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_04);
                } else if (battery >= 90 && battery <= 100) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_05);
                } else {
                    mn_iv_battery.setVisibility(View.GONE);
                }


            }
        }
    }

    private BatteryReceiver batteryReceiver;

    private void registerBatteryReceiver() {
        if (batteryReceiver == null) {
            //注册广播接受者
            IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            //创建广播接受者对象
            batteryReceiver = new BatteryReceiver();
            //注册receiver
            context.registerReceiver(batteryReceiver, intentFilter);
        }
    }

    private void unRegisterBatteryReceiver() {
        if (batteryReceiver != null) {
            context.unregisterReceiver(batteryReceiver);
        }
    }


非常简单,一目了然,这也是我选择从下而上为大家解析的原因,原作者的代码结构非常清晰,从方法定义入手反而更容易让我们吸收,而且可以培养我们写作代码的好习惯。写代码跟写文章一样,当你还不能自己写出或华丽、或深邃、或流畅的代码时,参考优雅的代码也是非常关键的开始。

下面我们再从开始看看作者为视频播放准备了哪些事情:

1、获取视频是否自动播放(我在项目过程中,大半时间花费在了自动播放这里,兼容性真是让人脑袋疼了又疼,大家以后写自动播放一定要定义好架构,否则就会跟我一样架构修改好几次,因为产品改需求了,这个是真无解除非你揍他)

private void initAttrs(Context context, AttributeSet attrs) {
        //获取自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MNViderPlayer);
        //遍历拿到自定义属性
        for (int i = 0; i < typedArray.getIndexCount(); i++) {
            int index = typedArray.getIndex(i);
            if (index == R.styleable.MNViderPlayer_mnFirstNeedPlay) {
                isFirstPlay = typedArray.getBoolean(R.styleable.MNViderPlayer_mnFirstNeedPlay, false);
            }
        }
        //销毁
        typedArray.recycle();
    }
2、转屏的时候重新计算视图大小(在实际过程中,这种方式只试用于非列表中,如果是列表,我使用的方式是定义一个全屏layout,在转屏后将SurfaceView放到全屏layout中,转屏回来后再设置回列表的parentview中,我会在讲解MNVideoPlayer后,将自己写的一个VideoPlayer再分享给大家):

@Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        int screenWidth = PlayerUtils.getScreenWidth(activity);
        int screenHeight = PlayerUtils.getScreenHeight(activity);
        ViewGroup.LayoutParams layoutParams = getLayoutParams();

        //newConfig.orientation获得当前屏幕状态是横向或者竖向
        //Configuration.ORIENTATION_PORTRAIT 表示竖向
        //Configuration.ORIENTATION_LANDSCAPE 表示横屏
        if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            //计算视频的大小16:9
            layoutParams.width = screenWidth;
            layoutParams.height = screenWidth * 9 / 16;

            setX(mediaPlayerX);
            setY(mediaPlayerY);
        }
        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            layoutParams.width = screenWidth;
            layoutParams.height = screenHeight;

            setX(0);
            setY(0);
        }
        setLayoutParams(layoutParams);
    }

3、实例化视图、手势、SurfaceView:

private void init() {
        View inflate = View.inflate(context, R.layout.mn_player_view, this);
        mn_rl_bottom_menu = (RelativeLayout) inflate.findViewById(R.id.mn_rl_bottom_menu);
        mn_palyer_surfaceView = (SurfaceView) inflate.findViewById(R.id.mn_palyer_surfaceView);
        mn_iv_play_pause = (ImageView) inflate.findViewById(R.id.mn_iv_play_pause);
        mn_iv_fullScreen = (ImageView) inflate.findViewById(R.id.mn_iv_fullScreen);
        mn_tv_time = (TextView) inflate.findViewById(R.id.mn_tv_time);
        mn_tv_system_time = (TextView) inflate.findViewById(R.id.mn_tv_system_time);
        mn_seekBar = (SeekBar) inflate.findViewById(R.id.mn_seekBar);
        mn_iv_back = (ImageView) inflate.findViewById(R.id.mn_iv_back);
        mn_tv_title = (TextView) inflate.findViewById(R.id.mn_tv_title);
        mn_rl_top_menu = (RelativeLayout) inflate.findViewById(R.id.mn_rl_top_menu);
        mn_player_rl_progress = (RelativeLayout) inflate.findViewById(R.id.mn_player_rl_progress);
        mn_player_iv_lock = (ImageView) inflate.findViewById(R.id.mn_player_iv_lock);
        mn_player_ll_error = (LinearLayout) inflate.findViewById(R.id.mn_player_ll_error);
        mn_player_ll_net = (LinearLayout) inflate.findViewById(R.id.mn_player_ll_net);
        mn_player_progressBar = (ProgressWheel) inflate.findViewById(R.id.mn_player_progressBar);
        mn_iv_battery = (ImageView) inflate.findViewById(R.id.mn_iv_battery);
        mn_player_iv_play_center = (ImageView) inflate.findViewById(R.id.mn_player_iv_play_center);

        mn_seekBar.setOnSeekBarChangeListener(this);
        mn_iv_play_pause.setOnClickListener(this);
        mn_iv_fullScreen.setOnClickListener(this);
        mn_iv_back.setOnClickListener(this);
        mn_player_iv_lock.setOnClickListener(this);
        mn_player_ll_error.setOnClickListener(this);
        mn_player_ll_net.setOnClickListener(this);
        mn_player_iv_play_center.setOnClickListener(this);

        //初始化
        initViews();

        if (!isFirstPlay) {
            mn_player_iv_play_center.setVisibility(View.VISIBLE);
            mn_player_progressBar.setVisibility(View.GONE);
        }

        //初始化SurfaceView
        initSurfaceView();

        //初始化手势
        initGesture();

        //存储控件的位置信息
        myHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mediaPlayerX = getX();
                mediaPlayerY = getY();
                Log.i(TAG, "控件的位置---X:" + mediaPlayerX + ",Y:" + mediaPlayerY);
            }
        }, 1000);
    }

    private void initViews() {
        mn_tv_system_time.setText(PlayerUtils.getCurrentHHmmTime());
        mn_rl_bottom_menu.setVisibility(View.GONE);
        mn_rl_top_menu.setVisibility(View.GONE);
        mn_player_iv_lock.setVisibility(View.GONE);
        initLock();
        mn_player_rl_progress.setVisibility(View.VISIBLE);
        mn_player_progressBar.setVisibility(View.VISIBLE);
        mn_player_ll_error.setVisibility(View.GONE);
        mn_player_ll_net.setVisibility(View.GONE);
        mn_player_iv_play_center.setVisibility(View.GONE);
        initTopMenu();
    }

    private void initLock() {
        if (isFullscreen) {
            mn_player_iv_lock.setVisibility(View.VISIBLE);
        } else {
            mn_player_iv_lock.setVisibility(View.GONE);
        }
    }

    private void initSurfaceView() {
        Log.i(TAG, "initSurfaceView");
        // 得到SurfaceView容器,播放的内容就是显示在这个容器里面
        surfaceHolder = mn_palyer_surfaceView.getHolder();
        surfaceHolder.setKeepScreenOn(true);
        // SurfaceView的一个回调方法
        surfaceHolder.addCallback(this);
    }

    private void initTopMenu() {
        mn_tv_title.setText(videoTitle);
        if (isFullscreen) {
            mn_rl_top_menu.setVisibility(View.VISIBLE);
        } else {
            mn_rl_top_menu.setVisibility(View.GONE);
        }
    }

其中注意的是SurfaceView是异步加载,所以我们需要监听SurfaceHolder的CallBack来确保我们的MediaPlayer准确的播放在SurfaceView中,即:

 //播放
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated");
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mediaPlayer.setDisplay(holder); // 添加到容器中
        //播放完成的监听
        mediaPlayer.setOnCompletionListener(this);
        // 异步准备的一个监听函数,准备好了就调用里面的方法
        mediaPlayer.setOnPreparedListener(this);
        //播放错误的监听
        mediaPlayer.setOnErrorListener(this);
        mediaPlayer.setOnBufferingUpdateListener(this);
        //第一次初始化需不需要主动播放
        if (isFirstPlay) {
            //判断当前有没有网络(播放的是网络视频)
            if (!PlayerUtils.isNetworkConnected(context) && videoPath.startsWith("http")) {
                Toast.makeText(context, context.getString(R.string.mnPlayerNoNetHint), Toast.LENGTH_SHORT).show();
                showNoNetView();
            } else {
                //手机网络给提醒
                if (PlayerUtils.isMobileConnected(context)) {
                    Toast.makeText(context, context.getString(R.string.mnPlayerMobileNetHint), Toast.LENGTH_SHORT).show();
                }
                //添加播放路径
                try {
                    mediaPlayer.setDataSource(videoPath);
                    // 准备开始,异步准备,自动在子线程中
                    mediaPlayer.prepareAsync();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        isFirstPlay = true;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //保存播放位置
        if (mediaPlayer != null) {
            video_position = mediaPlayer.getCurrentPosition();
        }
        destroyControllerTask(true);
        pauseVideo();
        Log.i(TAG, "surfaceDestroyed---video_position:" + video_position);
    }

demo作者将MediaPlayer实例化和销毁放到了SurfaceView的create方法和destroy方法中,我不建议这么做,而且这么做很有问题,就是SurfaceView的生命周期不是跟随Activity的,而是当SurfaceView视图可见/不可见的时候就会反复触发create和destroy方法,所以,我们可以在MNVideoPlayer实例化的时候就将MediaPlayer实例化,之后在SurfaceView的create回调用方法 mediaPlayer.setDisplay(holder),在destroy方法中使用 mediaplayer.setDisplay(null)来取消播放投影,这个大家会在我之后的项目中得到体现

4、各个视图的控制、点击事件及显示/隐藏、横竖屏操作,简单看一下即可:

private void unLockScreen() {
        isLockScreen = false;
        mn_player_iv_lock.setImageResource(R.drawable.mn_player_landscape_screen_lock_open);
    }

    private void lockScreen() {
        isLockScreen = true;
        mn_player_iv_lock.setImageResource(R.drawable.mn_player_landscape_screen_lock_close);
    }

    //下面菜单的显示和隐藏
    private void initBottomMenuState() {
        mn_tv_system_time.setText(PlayerUtils.getCurrentHHmmTime());
        if (mn_rl_bottom_menu.getVisibility() == View.GONE) {
            initControllerTask();
            mn_rl_bottom_menu.setVisibility(View.VISIBLE);
            if (isFullscreen) {
                mn_rl_top_menu.setVisibility(View.VISIBLE);
                mn_player_iv_lock.setVisibility(View.VISIBLE);
            }
        } else {
            destroyControllerTask(true);
        }
    }

    private void dismissControllerMenu() {
        if (isFullscreen && !isLockScreen) {
            mn_player_iv_lock.setVisibility(View.GONE);
        }
        mn_rl_top_menu.setVisibility(View.GONE);
        mn_rl_bottom_menu.setVisibility(View.GONE);
    }

    private void showErrorView() {
        mn_player_iv_play_center.setVisibility(View.GONE);
        mn_player_ll_net.setVisibility(View.GONE);
        mn_player_progressBar.setVisibility(View.GONE);
        mn_player_ll_error.setVisibility(View.VISIBLE);
    }

    private void showNoNetView() {
        mn_player_iv_play_center.setVisibility(View.GONE);
        mn_player_ll_net.setVisibility(View.VISIBLE);
        mn_player_progressBar.setVisibility(View.GONE);
        mn_player_ll_error.setVisibility(View.GONE);
    }

    private void setLandscape() {
        isFullscreen = true;
        //设置横屏
        ((Activity) context).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        if (mn_rl_bottom_menu.getVisibility() == View.VISIBLE) {
            mn_rl_top_menu.setVisibility(View.VISIBLE);
        }
        initLock();
    }

    private void setProtrait() {
        isFullscreen = false;
        //设置横屏
        ((Activity) context).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        mn_rl_top_menu.setVisibility(View.GONE);
        unLockScreen();
        initLock();
    }

OK,这篇文章到此结束,我不想一篇文章写的太长,结果大家看完之后脑袋都疼,下一篇我将为大家解析视频播放,缓存及预加载等相关信息

如何用FFmpeg编一个简单播放器详细步骤介绍 FFMPEG 是一个很好的库,可以用来创建视频应用或者生成特定的工具。FFMPEG 几乎为你把所有的繁重工作都做了,比 如解码、编码、复用和解复用。这使得 多媒体应用程序变得容易编。它是一个简单的,用 C 编的,快速的并且能够 解码 几乎所有你能用到的格式,当然也包括编码多种格式。 唯一的问题是它的文档基本上是没有的。有一个单独的指导讲了它的基本原理另 外还有一个使用 doxygen 生成的文档。这就是为什么当我决定研究 FFMPEG 来弄 清楚音视频应用程序是如何工作的过程中,我决定把这个过程用文档的形式记录 并且发布出来作为初学指导的原因。 在 FFMPEG 工程中有一个示例的程序叫作 ffplay。它是一个用 C 编的利用 ffmpeg 来实现完整视频播放的简单播放器。这个指导将从原来 Martin Bohme 一个更新版本的指导开始(我借鉴了一些),基于 Fabrice Bellard 的 ffplay, 我将从那里开发一个可以使用的视频播放器。在每一个指导中,我将介 绍一个 或者两个新的思想并且讲解我们如何来实现它。每一个指导都会有一个 C 源文 件,你可以下载,编译并沿着这条思路来自己做。源文件将向你展示一个真正 的 程序是如何运行,我们如何来调用所有的部件,也将告诉你在这个指导中技术实 现的细节并不重要。当我们结束这个指导的时候,我 们将有一个少于 1000 行代 码的可以工作的视频播放器。 在播放器的过程中,我们将使用 SDL 来输出音频和视频。SDL 是一个优秀的跨 平台的多媒体库,被用在 MPEG 播放、模拟器和很多视频游戏中。你将需要下载 并安装 SDL 开发库到你的系统中,以便于编译这个指导中的程序。 这篇指导适用于具有相当编程背景的人。至少至少应该懂得 C 并且有队列和互斥 量等概念。你应当了解基本的多媒体中的像波形一类的概念,但是你不必知道的 太 多,因为我将在这篇指导中介绍很多这样的概念。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值