Android 13 - Media框架(2)- Demo App与MediaPlayer Api了解

全新系列文章已更新:


尝试用MediaPlayer写了一个播放demo,实现了网络流和本地流的播放。由于本人对app开发一窍不通,所以demo中很多内容是边查资料边写的,写的也比较杂乱,能够帮助理解api就行。这一节主要会记录demo开发中学到的内容,以及了解MediaPlayer Api。

1、demo效果

由于Android Studio的虚拟设备只支持API 30,所以demo的编写是基于Android R的,但是后续看的代码还是会基于Android T,这部分应该差的不是很多。

demo代码还没有完善(已发现问题还没处理),目前实现的效果如下,包含有以下几个内容:

  • 网络视频以及本地视频播放
  • 本地视频的seek,播放时间更新
  • 播放过程中窗口最大化
    请添加图片描述

代码可在github下载:MediaPlayerDemo-github
有积分的小伙伴也可在CSDN下载:MediaPlayerDemo-CSDN


2、demo实现过程中学到的相关内容

2.1、layout

  • FrameLayout 中的控件默认位置都是在左上角,可以通过 layout_marginLeft/Right/Bottom/Top 来控制空间边缘的距离;
  • layout_gravity 用于控制组件在当前容器中的位置,可以设置top|right|bottom|left
  • LinearLayout 可以将组件水平或垂直摆放,用layout_weight可以动态调整组件的大小,这时候layout_width需要设置为0;
  • 同一个layout中后面的组件会覆盖前面的组件;
  • dp 转 px 的方法如下:
    public static int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

2.2、Manifest

如果需要访问内部存储,需要添加以下权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

同时application中还要添加:

android:requestLegacyExternalStorage="true"

打开app后需要给app赋予权限,否则仍不能访问存储。

访问Internet需要赋予如下权限:

<uses-permission android:name="android.permission.INTERNET" />

有了如上设定播放网络流仍会失败,还需在application添加:

android:usesCleartextTraffic="true"

播放页面横屏不能结束当前activity的生命周期,需要在activity中添加如下配置:

android:configChanges="orientation|screenSize"

android app默认的主题颜色是紫色,并且会有标题栏,我们可以修改 application 中的 theme主题,主题设置在themes.xml中。

2.3、Activity

这里要了解的是与Activity相关的方法,例如 onCreate、onStart、onPause、onResume、onDestory等,这些方法可以在Activity的基类AppCompatActivity中查找到,注意Override这些方法时需要调用它们的 super 方法。

启动MainActivity时,执行顺序如下:

2023-08-12 22:52:20.240 1790-1790/com.example.mediademo D/Demo_MainActivity: onCreate
2023-08-12 22:52:20.323 1790-1790/com.example.mediademo D/Demo_MainActivity: onPause
2023-08-12 22:52:20.324 1790-1790/com.example.mediademo D/Demo_MainActivity: onResume

启动VideoActivity时,执行顺序如下:

2023-08-12 22:59:32.992 1928-1928/com.example.mediademo D/Demo_MainActivity: onPause
2023-08-12 22:59:33.539 1928-1928/com.example.mediademo D/Demo_MainActivity: onStop

回到之前MainActivity时:

2023-08-12 22:59:41.757 1928-1928/com.example.mediademo D/Demo_MainActivity: onPause
2023-08-12 22:59:41.758 1928-1928/com.example.mediademo D/Demo_MainActivity: onResume

退出app时(按返回键或者退出后台),会多执行一个 onDestroy 销毁资源:

2023-08-12 23:06:09.275 2334-2334/com.example.mediademo D/Demo_MainActivity: dispatchKeyEvent, keycode = 4
2023-08-12 23:06:09.275 2334-2334/com.example.mediademo D/Demo_MainActivity: keyCode = 4
2023-08-12 23:06:09.383 2334-2334/com.example.mediademo D/Demo_MainActivity: dispatchKeyEvent, keycode = 4
2023-08-12 23:06:09.394 2334-2334/com.example.mediademo D/Demo_MainActivity: onPause
2023-08-12 23:06:09.940 2334-2334/com.example.mediademo D/Demo_MainActivity: onStop
2023-08-12 23:06:09.941 2334-2334/com.example.mediademo D/Demo_MainActivity: onDestroy

2.4、Handler Looper and Thread

由于我不是很熟悉java,也不是专业的android app开发工程师,所以关于这一小节的理解可能会有错误,如有小伙伴看到欢迎指出。以下是我对Thread、Handler、Looper的理解:

  • 每个 Activity 启动时都会自动开启一个线程,这个线程中应该执行了Looper.loop方法(我猜的),所有和 Activity 相关的事件均由这个 Looper 来做消息分发处理,这个线程也被称为 UI 线程;
  • 为什么一个线程 Thread 只能有一个 Looper 呢?因为 Looper.loop 是一个死循环,Thread.run 执行了这个方法当然就不能再去执行其他的内容了;
  • 为什么有的时候发消息用 Runnable,有的时候却用 Message 呢?我的理解是这样:用 Message 是让线程根据 Message.what 让 Handler 执行对应的操作,有的时候我们并不一定需要让Handler执行,工作可以直接在 Looper 中完成,这时候就用 Runnable;
  • 网上会有很多资料来讲主线程向子线程中发消息,子线程向主线程中发送消息,我觉得都没有理解到 Looper Handler 这块内容的本质。我的理解是这样,向某个线程发送消息,就是要将消息发到指定的 Looper 中,我们在创建 Handler 时会传入一个 Looper,我们利用对应线程的 Handler 就可以很轻松将 Message/Runnable 发送到指定线程中执行;当然我们也可用 Message.sendToTarget 将自身送到指定的 Looper;
  • HandlerThread 和 Thread 的区别在于前者在创建的时候会自动创建一个 Looper,而后者需要我们手动执行 Looper.prepare 以及 Looper.loop;
  • 网上会有很多资料来讲Handler的内存泄漏,这点我不是很能理解,Activity结束时为什么不去清理 Looper 中 MessageQueue 的内容呢?查看源码可以发现,我们可以在Activity结束时,在onDestory中调用Handler .removeCallbacksAndMessages 清除 MessageQueue 中由该 Handler 发送的内容;如果子线程中含有Looper,那么调用 Looper.quit 或者 Looper.quitSafely 可以安全退出子线程,同时可以调用 Thread.join 等待线程结束;如果用的是 HandlerThread ,我们可以调用 HanderThread.quitHanderThread.quitSafely 退出线程,这等同于调用 Looper.quit。按照我的理解,做到以上几点,内存泄露应该就不会发生了;
  • 以上内容的关键点在于退出 Activity 时能够打断 Run 函数,如果是UI线程我们不能主动打断,只能把 Handler 发出的消息清除;如果是子线程,我们可以通过打断 Looper 从而中断 Run 函数;

这里来看 MediaPlayer.java 中给我门提供的示例:

这里创建了一个 HandlerThread,来执行 addTrack 任务,任务执行完成后调用 Looper.quit退出线程。

        final HandlerThread thread = new HandlerThread("SubtitleReadThread",
              Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
        thread.start();
        Handler handler = new Handler(thread.getLooper());
        handler.post(new Runnable() {
        	...
     	    public void run() {
                int res = addTrack();
                if (mEventHandler != null) {
                    Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
                    mEventHandler.sendMessage(m);
                }
                thread.getLooper().quitSafely();
            }
        });

还有另外一个示例,MediaPlayer的创建过程中会创建一个EventHandler,reset 时会调用 Handler.removeCallbacksAndMessages 来清除 MessageQueue 中由 EventHandler 发送的消息。如果 Handler 用的主线程 Looper,那么主线程可以安全退出;如果用的子线程 Looper,还需要调用该线程的 quit 方法打断 loop。

	public MediaPlayer() {
		....
        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }
    }
    
    public void reset() {
    		...
            if (mEventHandler != null) {
            mEventHandler.removeCallbacksAndMessages(null);
        }
    }

如需更深入了解 Handler Looper and Thread 机制可以查看源码,也可以查看之前的 native AHandler ALooper 机制,原理大致是相同的 。


3、MediaPlayer Api分析

这一节主要是了解 MediaPlayer有什么api,我这里将api进行了罗列分组,并且贴出了他们的功能,至于他们要怎么用,有什么注意点,与生命周期相关的内容会在下一篇内容中了解。

首先是播放控制类api,这类api用于播放参数设定,以及Player生命周期的管理:

nummethodsFunc
1setDataSource设置数据源
2prepare准备
3prepareAsync准备
4start开始播放
5stop停止播放
6pause暂停播放
7release释放当前播放器持有的资源
8reset重置播放器
9finalize
10setPlaybackParams设置播放参数,例如倍速、Audio mode
11setSyncParams设置Avsync mode,有4种sync模式
12seekTo定位,有4种seek mode
13setDisplay设置 SurfaceHolder
14setSurface设置 Surface
15setVideoScalingMode设置缩放模式
16setAudioStreamType设置音频流类型
17invoke调用指定函数,no public,不支持App使用
18setParameter设定参数,no public,不支持App使用
19setVolume设置音量
20setAudioSessionId设置AudioSessionId
21selectTrack指定播放的track或者是切换当前播放的track

以下是参数获取类api,用他们可以获取到播放状态、播放参数等信息:

nummethodsFunc
1getVideoWidth获取宽度
2getVideoHeight获取高度
3isPlaying是否正在播放
4getPlaybackParams获取播放参数
5getSyncParams获取Avsync参数,里面包含有帧率等信息
6getTimestamp获取当前播放时间戳
7getCurrentPosition获取当前播放时间,单位是msec,getTimestamp是基于这个方法的
8getDuration获取视频时长
9getMetadata获取当前播放流的信息,no public,不支持app使用,包含 bitrate、mine、codectype等信息
10notifyAt设置pts更新的频率
11getAudioSessionId获取AudioSessionId
12getTrackInfo获取当前Track的媒体格式
13getSelectedTrack获取当前选择的track

下面两个api用于循环播放或者是快速播放:

nummethodsFunc
1setNextMediaPlayer设置下一个要播放的MediaPlayer,自动播放
2setLooping设置循环播放

以下类和方法用于回调事件的上抛与处理:

numclass/functionFunc
1EventHandlerframework回调事件的处理
2postEventFromNativenative callback to framework

以方法用于将回调事件进一步上抛给app层,app做相应动作:

numclassFunc
1OnPreparedListenerprepareAsync完成,搭配start使用
2OnCompletionListener当前码流播放完成
3OnBufferingUpdateListener缓冲进度更新
4OnSeekCompleteListenerseek完成
5OnVideoSizeChangedListener视频的宽高发生变化,搭配 getVideoWidth 和 getVideoHeight使用
6OnErrorListener错误事件回调
7OnTimedTextListener
8OnTimedMetaDataAvailableListener
9OnInfoListener播放器信息回调

关注公众号《青山渺渺》阅读完整内容; 如有问题可在公众号后台私信,也可进入音视频开发技术分享群一起讨论!

请添加图片描述

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青山渺渺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值