Android 13 - Media框架(3)- MediaPlayer生命周期

2023-10-27

上一节了解了MediaPlayer api的使用,这一节就我们将会了解MediaPlayer的生命周期与api使用细节。

1、MediaPlayer生命周期

MediaPlayer.java 一开始有对生命周期的描述,这里对这些内容进行翻译:

  1. MediaPlayer 是线程不安全的,创建以及调用应该在同一个线程中,如果需要使用Callback,那么线程必须要有一个Looper;
  2. 调用 new 或者 reset 后MediaPlayer进入 Idle 状态,release 调用后进入到 End 状态,这两个状态之间就是 MediaPlayer 对象的生命周期;
  3. 调用 new 或者 reset都会进入Idle 状态,但是它们仍然是有区别的。getDuration 等方法在Idle 状态下调用会出现error,如果是new之后调用,虽然会返回error,但是error并不是由内部播放器引擎上抛的,播放器状态仍然保持在Idle 状态;如果刚好是在reset之后调用getDuration方法,那么内部播放器引擎会上抛error事件,并且把播放器状态置为Error
  4. 如果MediaPlayer对象不再需要被使用,推荐调用release方法,让内部播放器引擎使用的资源能够被释放;一旦进入了 End 状态,MediaPlayer对象将不能再被使用,并且不能再被切换到其他状态,如果仍要使用需要重新new一个对象,进入Idle状态。
  5. 有很多原因会造成播放失败,比如不支持的audio/video格式、分辨率过高、streaming超时等,这些错误会让播放器进入Error状态并且上抛事件,如果要继续使用当前MediaPlayer对象,可以调用reset使其进入到Idle状态;
  6. setDataSource会让播放器从Idle进入Loaded状态,如果在其他状态调用则会抛出IllegalStateException错误;
  7. prepare会让播放器从Loaded状态进入到Prepared状态,如果调用的是prepareAsync,则会先进入中间状态Preparing;在除了LoadedStopped状态外调用prepare方法都是非法的,会抛出IllegalStateException错误;
  8. start方法会让播放器从Prepared状态进入到Started状态,可以用isPlaying判断播放器当前状态是否在Started状态;如果已经进入到Started状态,则再调用start不会有任何影响;
  9. pause方法会让播放器进入到Paused状态,从Started进入到Paused或者反过来的过程都是异步的;如果已经进入到Paused状态,则再调用pause不会有任何影响;在Paused状态下重新调用start将会恢复播放,进入到Started状态;
  10. stop会让播放器从StartedPausedPreparedPlaybackCompleted状态进入到Stopped状态,一旦进入到Stopped状态,那么必须要调用prepare重新进入Prepared状态才能够重新进行播放;同样的,如果已经进入到Stopped状态,则再调用stop不会有任何影响;
  11. seekTo方法是异步的,调用完成后会有callback事件onSeekComplete上抛,seekTo可以在StartedPausedPreparedPlaybackCompleted状态下调用;
  12. 当播放结束时,如果setLooping设置为true,那么播放器将会保持在Started状态,如果setLooping设置为false,那么播放器将会进入到PlaybackCompleted状态;在PlaybackCompleted状态下,调用start方法将会从头开始重新播放,并且进入到Started状态。

2、异常处理

从上面我们可以了解到,MediaPlayer维护了一套状态机,并且调用MediaPlayer方法时会检查当前调用是否是非法的,这套状态机机制在 mediaplayer.cpp 中。 MediaPlayer 定义了如下状态:

enum media_player_states {
    MEDIA_PLAYER_STATE_ERROR        = 0,
    MEDIA_PLAYER_IDLE               = 1 << 0,
    MEDIA_PLAYER_INITIALIZED        = 1 << 1,
    MEDIA_PLAYER_PREPARING          = 1 << 2,
    MEDIA_PLAYER_PREPARED           = 1 << 3,
    MEDIA_PLAYER_STARTED            = 1 << 4,
    MEDIA_PLAYER_PAUSED             = 1 << 5,
    MEDIA_PLAYER_STOPPED            = 1 << 6,
    MEDIA_PLAYER_PLAYBACK_COMPLETE  = 1 << 7
};

要注意的是,End状态表示native MediaPlayer对象已经销毁了,所以它并没有真正的 End 状态。

MediaPlayer.java 中还有一段关于函数在什么状态下调用是有效的,什么状态下调用是无效的的表格,这里挑出一些进行翻译。

序号 方法 有效状态 无效状态下调用
1 getAudioSessionId any
2 getCurrentPosition Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted Error
3 getDuration Prepared, Started, Paused, Stopped, PlaybackCompleted Error
4 getVideoHeight any
5 isPlaying any
6 pause Started, Paused, PlaybackCompleted Error
7 prepare Initialized, Stopped 异常
8 prepareAsync Initialized, Stopped 异常
9 release any
10 reset any
11 seekTo Prepared, Started, Paused, PlaybackCompleted Error
12 setDataSource Idle 异常
13 setDisplay any
15 setSurface any
16 setVideoScalingMode Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted
17 setLooping Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted
18 start Prepared, Started, Paused, PlaybackCompleted Error
19 stop Prepared, Started, Stopped, Paused, PlaybackCompleted Error

从以上表格我们可以发现,非法状态下调用prepareprepareAsyncsetDataSource会抛异常中止程序运行;调用startpause等方法会将状态置为 Error,具体程序如何反应需要看我们的OnErrorListener是如何处理的:

    case MEDIA_ERROR:
        Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
        boolean error_was_handled = false;
        OnErrorListener onErrorListener = mOnErrorListener;
        if (onErrorListener != null) {
            error_was_handled = onErrorListener.onError(mMediaPlayer, msg.arg1, msg.arg2);
        }
        {
            mOnCompletionInternalListener.onCompletion(mMediaPlayer);
            OnCompletionListener onCompletionListener = mOnCompletionListener;
            if (onCompletionListener != null && ! error_was_handled) {
                onCompletionListener.onCompletion(mMediaPlayer);
            }
        }
        stayAwake(false);
        return;

3、对new、release、reset的一些理解

相关代码路径:

3.1、new

在看正式的 new MediaPlayer 流程前,我们先要注意MediaPlayer.java中的如下代码段:

    static {
        System.loadLibrary("media_jni");
        native_init();
    }

它会加载media_jni.so,并执行native函数 android_media_MediaPlayer_native_init 。native_init会获取java类中的postEventFromNative方法ID,mNativeContextmNativeSurfaceTexture等字段的ID,获取到的ID会存储在静态变量fields_t中,后期可以通过这些ID找到Java对象中对应的成员,获取其存储的值或者向其中存储值。

static fields_t fields;

fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative", "(Ljava/lang/Object;IIILjava/lang/Object;)V");
fields.surface_texture = env->GetFieldID(clazz, "mNativeSurfaceTexture", "J");

接下来看new MediaPlayer创建一个对象会做哪些事情:

private MediaPlayer(int sessionId) {
	...
	try (ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()) {
		native_setup(new WeakReference<MediaPlayer>(this), attributionSourceState.getParcel());
	}
}

核心是调用native_setup方法,需要将自身的弱引用对象作为参数向下传递:

static void
android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
                                       jobject jAttributionSource)
{
    Parcel* parcel = parcelForJavaObject(env, jAttributionSource);
    android::content::AttributionSourceState attributionSource;
    attributionSource.readFromParcel(parcel);
    // 创建MediaPlayer native对象
    sp<MediaPlayer> mp = sp<MediaPlayer>::make(attributionSource);
	// 创建并注册Callback对象
    sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this);
    mp->setListener(listener);
	// 将MediaPlayer对象存储到Java对象中
    setMediaPlayer(env, thiz, mp);
}

native_setup干了3件事:

  1. 创建MediaPlayer native对象;
  2. 用传下来的弱引用对象创建Listener对象,用于Callback调用;
  3. 将MediaPlayer native对象指针到Java对象中;

来看看如何存储MediaPlayer native指针的:

static sp<MediaPlayer> setMediaPlayer(JNIEnv* env, jobject thiz, const sp<MediaPlayer>& player)
{
    Mutex::Autolock l(sLock);
    sp<MediaPlayer> old = (MediaPlayer*)env->GetLongField(thiz, fields.context);
   	// 手动增加强引用计数
    if (player.get()) {
        player->incStrong((void*)setMediaPlayer);
    }
    // 检查mNativeContext中存储的MediaPlayer对象
    if (old != 0) {
        old->decStrong((void*)setMediaPlayer);
    }
    // 将MediaPlayer的地址存储到mNativeContext
    env->SetLongField(thiz, fields.context, (jlong)player.get());
    return old;
}

这里有3个步骤:

  1. 首先要增加MediaPlayer native对象的强引用计数;
  2. 检查mNativeContext中存储的地址是否为NULL,如果不为NULL则需要销毁存储的MediaPlayer对象;
  3. 将新的MediaPlayer对象存储到mNativeContext中。

虽然我们会把MediaPlayer对象存储到Java字段中,但是其引用计数在setMediaPlayer中仍为1,如不增加引用计数,出了当前作用域native对象将自动销毁。

3.2、reset

reset的作用是重置当前MediaPlayer对象,使其恢复到new的状态。我们来看下reset实现:

status_t MediaPlayer::reset_l()
{
    mLoop = false;
    if (mCurrentState == MEDIA_PLAYER_IDLE) return NO_ERROR;
    // 1.阻止 MEDIA_PREPARED 事件上抛
    mPrepareSync = false;
    if (mPlayer != 0) {
    	// 2.调用内部实现的reset
        status_t ret = mPlayer->reset();
        if (ret != NO_ERROR) {
            ALOGE("reset() failed with return code (%d)", ret);
            mCurrentState = MEDIA_PLAYER_STATE_ERROR;
        } else {
        	// 2.调用内部实现的disconnect
            mPlayer->disconnect();
            mCurrentState = MEDIA_PLAYER_IDLE;
        }
        // 3.销毁内部实现
        mPlayer = 0;
        return ret;
    }
    // 4.清除设置
    clear_l();
    return NO_ERROR;
}

reset主要会做3件事情:

  • 如果当前状态是Preparing,那么会将 mPrepareSync 置为fasle,阻止 MEDIA_PREPARED 事件上抛,具体内部如何操作后续再研究;
  • 调用 MediaPlayer 内部实例的 disconnect 方法断开连接,如果处在播放状态将会停止播放(这点后续再研究);
  • 销毁 MediaPlayer 内部实例;

3.3、release

release会直接销毁掉MediaPlayer native对象,一旦调用release,当前MediaPlayer java对象将不能够再被使用:

MediaPlayer::~MediaPlayer()
{
    ALOGV("destructor");
    if (mAudioAttributesParcel != NULL) {
        delete mAudioAttributesParcel;
        mAudioAttributesParcel = NULL;
    }
    AudioSystem::releaseAudioSessionId(mAudioSessionId, (pid_t)-1);
    disconnect();
    IPCThreadState::self()->flushCommands();
}

MediaPlayer 的析构函数同样也会调用内部实例的 disconnect 方法断开连接,之后会销毁 MediaPlayer 内部实例,这两点和reset是相同的。我们在想要结束activity,可以先调用reset,再调用release,也可以直接调用release来释放资源。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android 13 - Media框架(3)- MediaPlayer生命周期 的相关文章

随机推荐

  • org-mode报错:export html等格式时,错误args out of range

    错误概述 参数超出范围 解决过程 相关命令没有任何改动 原因 org的内容有问题 当前遇到的是表格写法 不符合org mode的规则
  • Linux下保存的SVN帐号及密码

    做个笔记备忘 Linux下SVN帐号和密码 如果有保存的话 大多是明文保存在 subversion auth svn simple的 因此如果服务器帐号和密码被人知道 并且你有在服务器上使用过或者还保存过密码 那么就帐号和密码就很容易被人一
  • cocos2dx使用TiledMap创建斜45度地图场景

    做游戏 场景是一个很重要的部分 如果缺少这一步 很难做出好的游戏 对于cocos2dx来说 有很多2D的地图编辑器可以用 效果都还可以 其中Tiled是支持的比较好的 它支持Tiled编辑出来的几种模式 比如正常 45度地图等 如果要做小型
  • vim的目录树插件NERDtree的安装

    下载 https github com preservim nerdtree 上面是NERDTree插件的下载链接 在github上下载即可 将下载的文件的解压 并通过虚拟机的共享文件夹共享到虚拟机 将共享的文件 复制到 vim 目录下 如
  • USB Type C 接口引脚详解

    1 Type C 接口特点 Type C 是一组对称的连接器 在使用的过程中不需要如同使用 USBA MinUSB MicroUSB 那样来辨别接口方向 其次能够承受较高的功率所以可以支持高达 100W 的功率 所以使用该接口可以更好的支持
  • 骁龙435和骁龙625处理器哪个好?

    2016年2月 美国高通公司推出了三款中低端芯片 它们分别是骁龙425 骁龙435 骁龙625 这三款芯片配备更快的LTE网络基带 可实现全网通 骁龙425市场表现比较平淡 而骁龙625是目前较为热销的手机芯片 其中骁龙435也逐渐在市场活
  • Linux文件编程常用函数详解——wait()函数

    函数原型和头文件 include
  • 【JavaScript】判断对象是否有某个key

    1 in方法 实例属性 继承属性 key in obj 结果为false 表示不包含 否则表示包含 2 hasOwnProperty 实例属性 obj hasOwnProperty key obj表示对象 结果为false表示不包含 否则表
  • 充换电企业开迈斯低成本提升线上应用稳定性的最佳实践

    开迈斯新能源科技有限公司于 2019 年 5 月 16 日成立 目前合资股东分别为大众汽车 中国 投资有限公司 中国第一汽车股份有限公司 一汽 大众汽车有限公司 增资扩股将在取得适当监督 包括反垄断 审批后完成 万帮数字能源股份有限公司和安
  • 1.4秒到0.4秒-2行代码让JS加载耗时减少67%

    大厂技术 高级前端 Node进阶 点击上方 程序员成长指北 关注公众号 回复1 加入高级Node交流群 前言 大家好 我是考拉 资源优先级优化效果示例 png 仅需2行代码 就能实现上图中的优化效果 让JS文件的加载耗时从1 4秒减少到0
  • 七:微服务调用组件Feign

    目录 JAVA 项目中如何实现接口调用 1 什么是Feign 1 1 优势 1 2 Feign的设计架构 1 3 Ribbon Feign对比 1 4 Feign单独使用 2 Spring Cloud Alibaba快速整合Feign 3
  • 获取kafka队列中待消费Lag

    获取kafka中消费者某个topic主题待消费数据量 import java util ArrayList import java util HashMap import java util List import java util Ma
  • Invalid content was found starting with element ‘{“http://maven.apache.org/POM/4.0.0“:dependency}‘.

    在maven项目中运行时出现如下错误 点击上面的项目名 点击链接进入到报错的文件中 一般这种报错就是你的 Invalid content was found starting with element http maven apache o
  • java web项目中连接mysql数据库,javaweb之eclipse工程连接mysql数据库

    javaweb之eclipse工程连接mysql数据库 准备工作 1 在mysql官网下载mysqlconnection的jar包 输入网址 mysql com 点击DOWNLOADS 下拉选择MySQL Community GPL Dow
  • 入坑爬虫(六)某招聘网站信息采集

    前面的章节中 我们说到了如何发送发送 对应的 回顾之前的爬虫流程 在发送完请求之后 能够获取响应 这个时候就需要从响应中提取数据了 1 爬虫中数据的分类 在爬虫爬取到的数据中有很多不同类型的数据 我们需要了解数据的不同类型来规律的提取和解析
  • ADB简介

    Google官方网页 https developer android com studio command line adb html hl zh cn 对ADB的介绍在国内经常打不开 为了便于查看 这里从此网页中摘录了些经常使用到的内容
  • 2021杭电多校第三场-Road Discount-wqs二分+最小生成树

    Description There are n cities in Byteland labeled by 1 to n The Transport Construction Authority of Byteland is plannin
  • getLocation:fail Coordinate address resolution failed -1501

    这表示在获取位置信息时出现错误 具体原因可能是坐标地址解析失败 错误代码 1501 表示这种情况 你可以尝试重新请求位置信息 或者检查你的设备是否能够访问位置信息 例如 确保位置服务已打开
  • java swing实现文件浏览器功能小程序

    闲来无事学习了一下java的桌面应用开发组件Swing 做了个小程序 文件浏览器 只能查看信息不能进行过多操作 文件功能 查看指定文件路径下的所有文件夹和文件 可查看文件夹和文件的详细信息 切换不同排列方式等 基本逻辑 设置列表布局 添加数
  • Android 13 - Media框架(3)- MediaPlayer生命周期

    上一节了解了MediaPlayer api的使用 这一节就我们将会了解MediaPlayer的生命周期与api使用细节 1 MediaPlayer生命周期 MediaPlayer java 一开始有对生命周期的描述 这里对这些内容进行翻译