荏苒追寻个人博客

做一个有追求的青年


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 日程表

Android 音视频基础1——AudioTrack、AudioRecorder、MediaRecorder音频数据采集播放处理

发表于 2021-01-02 | 分类于 Android , 音视频基础

简介

  • AudioRecorder,可以采集PCM格式的原始音频文件,对PCM格式数据,根据一定的协议约定,可以转换成wav格式等其他格式;
  • AudioTrack,可以直接播放PCM格式的文件,也就是说上面利用AudioRecorder生成的原始文件可以通过AudioTrack直接播放;
  • MediaRecorder,可以录制音频,也可以配合相机(Camera)录制视频,相比较而言,MediaRecorder功能更丰富,更易用,使用场景也更多,而AudioRecorder和AudioTrack则更底层一点,且功能存在局限性。
阅读全文 »

Android 工具命令——发布项目到Maven

发表于 2020-12-31 | 分类于 Android , 工具命令

本文链接:https://rainmonth.github.io/posts/A201231.html

将项目发布到Maven,根据自己的需求,可以有以下三种方式:

  • 发布到本地Maven仓库,即在自己的电脑上共享;
  • 发布到本地局域网,即局域网内用户共享;
  • 发布到JCenter,即全网共享;

上面三种方式大同小异,具体可继续往下看。

阅读全文 »

Android 工具命令——发布项目到JitPack

发表于 2020-12-30 | 分类于 Android , 工具命令

主要分以下几步:

  1. 本地创建Library工具类;
  2. 配置JitPack相关信息;
  3. 排查Library中错误并上传至GitHub;
  4. 创建release并在JitPack中编译;
  5. 在项目中引用
阅读全文 »

Android 开源库分析——LeakCanary分析

发表于 2020-08-11 | 分类于 Android

本文链接:https://rainmonth.github.io/posts/A200811.html
LeakCanary本质上就是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具。

注意:本文基于1.6.3版本进行分析。

内存泄漏检测的原理

主要就是利用检测对象的声明周期(Activity和Fragment)回调中开启监听,依据弱引用在gc触发后会被加入到ReferenceQueue中,然后根据自己记录的要回收的对象key和已经加入到ReferenceQueue中的对象key做对比,如果自己记录的key中包含了ReferenceQueue中不存在的内容,则说明有内存泄漏发生了。

阅读全文 »

Andorid 系统架构——Jetpack实践

发表于 2020-07-25 | 分类于 Android , 系统架构

https://rainmonth.github.io/posts/A200725.html

简介

Jetpack 是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

Jetpack 包含与平台 API 解除捆绑的 androidx.* 软件包库。这意味着,它可以提供向后兼容性,且比 Android 平台的更新频率更高,以此确保您始终可以获取最新且最好的 Jetpack 组件版本。

Android 开源库分析——Dagger2简介

发表于 2020-07-01 | 分类于 Android , 开源库分析

本文链接:https://rainmonth.github.io/posts/A200701.html

摘要

Dagger是一个编译时依赖注入框架。实现原理是在编译的时候根据注解生成相应的Java源代码,然后在利用这些源代码进行程序的处理,所以并不会降低程序运行时的性能。

Dagger解决了什么问题呢?就是对象之间的耦合。将直接的组合方式造成的耦合,改为间接的依赖关系,从而降低耦合。

阅读全文 »

通用功能——Mac 常见问题及解决方案

发表于 2020-04-24 | 分类于 通用

摘要

本文主要记录Mac使用及开发过程中遇到的一些常见问题及解决方案

阅读全文 »

Andorid 工具命令Repo 命令简介

发表于 2020-03-25 | 分类于 Android , 工具命令

本文链接:https://rainmonth.github.io/posts/A200325.html

Android Repo 命令简介

通用讲解参见官方文档

常用格式如下

1
repo command option

help

查看帮助,使用如下:

1
2
3
4
5
6
# 查看所有帮助
repo help
# 查看init命令的帮助
repo help init
# 查看init命令支持哪些选项
repo init --help

init

类似于git的init,该命令在当前目录创建一个.repo文件夹,常用options:

  • -u, 指定要更新manifest 仓库的位置,可见这是一个manifest的地址
  • -m,指定初始化的manifest文件
  • -b,指定manifest的分支或版本

主要用来切换分支,切换分支的一般步骤:

  1. 在aosp(源码根目录)下,执行如下命令即可查看可切换的分支
1
2
cd .repo/manifests
git branch -a | cut -d / -f 3
  1. 切换分支(以android-7.0.0_r1为例),这里的地址记得换成自己的镜像源
1
repo init -u https://android.googlesource.com/platform/manifest -b android-7.0.0_r1
  1. 同步代码
1
repo sync

sync

常用方式

1
repo sync [project_list]

从remote端下载新近更改并更新本地工作目录,相当于对所有git项目运行git fetch命令。如果没指定参数,同步所有项目。直接运行repo sync,可能会发生如下结果:

  • 若从未同步过项目,repo sync等同于git clone,remote上的所有分支都将下载到本地;

  • 若同步过,repo sync等同于以下命令的组合:

    1
      

Android 适配——全局悬浮窗的实现

发表于 2020-03-12 | 分类于 Android , 适配

https://rainmonth.github.io/posts/A200312.html

摘要

悬浮窗是App开发中比较常用的功能,本文就探究下Android中悬浮窗的实现方案。这里主要讨论以下两种方案:全局悬浮窗和应用内悬浮窗。两种实现方式各有优缺点(后面会有所比较),本文先对两种方案的实现思路分析,然后给出具体的编码实现,最后将二者封装完成,做成轮子可以按需使用。

要实现的功能

封装后的悬浮窗需具备以下功能:

  • [x] 支持应用内所有页面悬浮;
  • [x] 支持应用外全局悬浮;
  • [x] 支持可跟随手指移动;
  • [ ] 支持配置吸附效果;
  • [x] 支持设置悬浮View动效;
  • [x] 悬浮窗权限适配问题
  • [ ] 显示黑名单与白名单
  • [ ] 支持数据绑定(采用泛型实现)
  • [x] 同时支持多个悬浮窗,并且可以对他们进行管理
  • [x] 支持自定义视图

需要解决的问题

要完成上述功能,需要解决以下几个问题

  • 权限适配的问题(全局悬浮窗在Android API 在23(Android 6.0)以上时需要动态请求权限,其他情况下不需要请求权限);
  • 悬浮窗的降级处理(在权限未获取到的情况下,如果请求的时全局悬浮窗,则直接采用应用内悬浮窗实现,目前好多主流大厂都是这样实现的);
  • 多个悬浮窗的管理(悬浮窗管理类中维护一个Map,key为悬浮窗的id,value为悬浮窗的配置,id唯一,可以通过这个id来对悬浮窗进行管理);
  • 悬浮窗的内存管理(管理类采用单例模式实现,而这个单例里面存在悬浮View的使用,所以要注意内存的管理,主要是要防止内存泄漏);

全局悬浮窗

实现方式

请求权限,然后通过WindowManager的addView来添加悬浮View,同时通过WindowManager.LayoutParams类进行View配置的更新

权限请求

全局悬浮窗需要动态申请权限,主要注意一下两点。

  1. 不管哪个Android系统版本,AndroidManifest.xml文件中必须要声明android.permission.SYSTEM_ALERT_WINDOW权限的使用;

  2. Android 6.0以上,除要声明上述权限外,还要再运行时动态申请权限,权限请求代码如下:

    1
    2
    3
    4
    5
    6
    7
    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
    intent.setData(Uri.parse("package:" + Utils.getApp().getPackageName()));
    if (!UtilsBridge.isIntentAvailable(intent)) {
    launchAppDetailsSettings();
    return;
    }
    activity.startActivityForResult(intent, requestCode);

    注意:Android 6.0后系统对某些敏感权限要求动态请求,仅仅在AndroidManifest.xml中声明是远远不够的,需要运行时动态申请,而这种动态请求权限中又有两个权限的请求方式比较特殊,悬浮窗权限就是其中之一,不能通过Activity.requestPermissions方法来完成,而是需要通过Intent打开页面来进行设置

全局悬浮View的配置

全局悬浮View的显示配置主要通过WindowManager.LayoutParams来进行控制,对于这个WindowManager.LayoutParams,有几点需要注意:

  • type指定的时悬浮窗的类型,这里需要注意的时Android 6.0以上这个值需要指定为TYPE_APPLICATION_OVERLAY,其他指定为TYPE_PHONE即可;
  • gravity默认值为Gravity.START | Gravity.TOP,采用默认值可以避免不必要的坐标转换计算,WindowManager.LayoutParams中gravity的默认值为Gravity.CENTER,如果要想使下面的通过x、y来控制窗口的显示位置,那么久必须设置这个值并且这个值不能是Gravity.CENTER;
  • x,设置window的x轴偏移值(在Gravity未设置或者未默认值Gravity.CENTER时不生效);
  • y,设置window的y轴偏移值(在Gravity未设置或者未默认值Gravity.CENTER时不生效);
  1. 通过WindowManager添加的View无法显示在屏幕以外的区域,而通过ViewGroup添加的View可以;
  2. 上面的参数中,同一个悬浮窗的type是不能更改的;

View的配置例如位置更新等,主要通过WindowManager的updateViewLayout(View view, ViewGroup.LayoutParams params)方法来实现,其中有一个参数需要注意

应用内全局悬浮

实现方式

通过动态的向Activity根布局添加或移除View来实现悬浮窗;

悬浮View的配置

通过FrameLayout.LayoutParams类进行View配置(位置相关的配置)的更新。

两种方式总结

全局悬浮

  • 优点,可以始终显示在最上端,不存在应用内悬浮页面切换闪烁的问题;
  • 缺点,6.0以上需要动态申请权限,如果用户拒绝该权限,需要做相应的降级处理

应用内悬浮

  • 优点,无需申请权限,所有版本完美运行;
  • 缺点,不能做到应用外悬浮(即应用推到后台后不能悬浮),由于是动态的添加和移除View,在切面切换时会出现闪烁的现象(需要在旧的页面移除,然后在新的页面添加),5.0之后可以共享元素来进行避免。

具体实现

关键的类

  • FloatViewManager,悬浮View管理(采用单例模式),FloatViewManager中根据悬浮窗id来记录相应的FloatView,同时也根据悬浮窗id记录相应的FloatView配置;
  • FloatViewConfig,悬浮View配置项管理(采用Builder模式)
  • FloatViewContainer,主要负责实现随手指滑动、停靠功能,这里全局悬浮和应用内悬浮需要分开设置,应为全局悬浮更新的时Window的坐标,而应用内悬浮跟新的时View的坐标,每个FloatViewContainer都有一个与之对应的FloatViewConfig
  • FloatPermissionUtils,主要负责全县请求方面的工作
  • Utils,主要负责实现常用的工具方法

悬浮View的设计

悬浮View的配置

悬浮View的配置交给FloatViewConfig类来实现,将上面要实现的功能
采用Builder模式

  1. 支持配置是否可以应用外悬浮(应用外悬浮需要支持动态配置,同时要提供关闭的选项);
  2. 支持配置自动停靠(配置了就会就近停靠到相应的位置);
  3. 支持自定义View;
  4. 支持配置悬浮View的大小;
  5. 支持是否显示关闭按钮(为什么会有这么一个设计呢?因为做demo的时候,发现如果申请了弹窗权限,存在应用销毁了,但是悬浮窗仍存在的现象,这有点流氓)

这里指的就是FloatDragLayout的设计,核心就是一个ViewGroup,需要支持一下功能:

  1. 支持随手指移动;
  2. 支持自动靠边停靠;
  3. 支持几种简单的动效
    1. 上下浮动;
    2. 左右抖动;
    3. 旋转动效

相关文章

  • Android应用内悬浮窗的实现方案
  • Android应用内悬浮窗从入门到放弃/妥协
  • Andorid 应用内悬浮控件实践方案总结
  • Andorid 应用内悬浮控件实践方案总结
  • 应用内悬浮窗(不请求权限))
  • Android无需权限显示悬浮窗, 兼谈逆向分析app

问题记录

  1. 调用requestPermission方法获取SYSTEM_ALERT_WINDOW权限一直失败,失败的原因就是这个权限不能通过requestPermission来申请,需要先在AndroidManifest.xml文件中声明,然后通过隐式Intent来请求权限,代码如下:

    1
    2
    3
    4
    5
    6
    7
    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
    intent.setData(Uri.parse("package:" + Utils.getApp().getPackageName()));
    if (!UtilsBridge.isIntentAvailable(intent)) {
    launchAppDetailsSettings();
    return;
    }
    activity.startActivityForResult(intent, requestCode);
  2. 悬浮窗显示出来后,除了悬浮窗显示的View及物理按键,其他区域都无法响应事件,问题原因是因为WindowManager.LayoutParams中的flags参数设置有问题,需要添加如下设置:

    1
    layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
  3. 应用销毁了,但悬浮窗仍然存在

  4. Android permission denied for window type 2002

    Android M (6.0)全局悬浮窗(通过请求SYSTEM_ALERT_WINDWO权限)window的type不能设置为PHONE,可以设置成APPLICATION_OVERLAY

Android 音视频基础——官方关于多媒体的说明介绍

发表于 2019-12-08 | 分类于 Android , 音视频基础

https://rainmonth.github.io/posts/A191208.html

摘要

最近在研究Android多媒体开发,于是就看了下官方关于多媒体的一些开发指南,本文对指南的主要内容做一些记录,以便实际开发时参考。参考文章:

  • 点击查看MediaPlayer的API介绍
  • 点击查看官方多媒体开发指南
  • 打造基于MediaSessionCompat的音乐播放器

MediaPlayer介绍

状态机

音视频文件(或流)的播放控制是通过下图所示的状态机来管理的,图例说明:

  • 椭圆代表的是播放器可能达到的状态;
  • 弧线代表的是驱动状态改变的控制操作;
    • 单箭头的弧线代表的是同步调用;
    • 双箭头的弧线代表的是异步调用;

MediaPlayer状态图

关于播放器状态图的几点说明:

  • 生命周期(Idle开始,End结束),当采用new关键字创建播放器或者调用播放器的reset方法后,它将处于Idle状态,当播放器调用release()方法后,它将处于End状态。从Idle到End这个过程,就是播放器的生命周期。
    • Idle状态下(不论是new创建的还是调用reset得到的)调用播放器操作(如play、pause等)被认为是编码错误,这种情况(即一新建对象就调用prepare等方法)。但两种情况有些许区别,通过new达到的Idle状态时用户提供的OnErrorListener.onError()不会被播放器内核调用,并且播放器状态不会改变;但通过reset达到Idle时OnErrorListener.onError()会被调用,并且播放器状态且切换至Error状态;
    • 当不再使用播放器是,强烈建议调用release方法以释放播放器对象关联的播放内核引擎资源。原因是这些资源会持有硬件加速组件的单例引用。当调用release失败时会让播放器回滚至软件加速实现(或者两者都失败),这样就进入到了End状态,而一旦播放器进入End状态,就不能再改变其状态了。
    • 补充一点,使用new得到的播放器处于Idle状态,但使用重载的create系列方法得到的播放器却处于prepared状态(前提是创建成功了)。
  • 导致播放操作调用失败的原因有很多,如播放格式不支持、分辨率不支持(太高)、播放流响应时间太长等,因此错误报告处理和播放器状态恢复就十分重要。但是上面说了,错误的调用时有发生(如上面提到的情况),为了保证这种情况我们能获取到回到通知,我们可以通过setOnErrorListener相关提前注册错误监听器,这样即使是由于我们在错误的状态调用博昂起操作,也能得到错误通知。
    • 通常情况下,即使我们没有提供错误监听器,播放器一旦错误就会进入Error状态(当然上面提到的那种情形除外);
    • 为了复用对象,一旦错误,我们可以调用reset方法来进行恢复;
    • 在应用中提前注册错误监听是个好习惯;
    • 为防止编码错误,在错误的状态调用prepare、prepareAsync、setDataSource等方法,会抛出IllegalStateException;
  • Idle状态下调用setDataSource系列方法成功后会将播放器状态从Idle改变成Initialized;
  • 在播放之前,播放器必须先到到Prepared状态,达到后如果有注册OnPreparedLister,其onPrepared()方法会被调用,有两种途径:
    • 通过调用prepare方法和通过调用prepareAsync方法(先到Preparing状态,再到Prepared状态)
    • Preparing是一个过渡状态
    • 非Initialize和Stopped状态下调用prepare和prepareAsync会抛出IllegalStateException异常;
    • Prepared状态下,可以通过调用相应方法来调整音量、保持屏幕常亮、循环控制等;
  • 成功调用start方法后进入Started状态,可以通过isPlaying()来判断是否处于Started状态
    • Started状态下可以通过设置的OnBufferingUpdateListener.onBufferingUpdate来跟踪缓存的状态;
    • 播放器已经处于Started状态是再调用start方法对播放状态没有影响;
  • 播放时可以暂停和停止,同时可以调整播放的位置(分别对应pause、stop、seekTo三个方法),pause成功调用后,便进入了Paused状态,从Started状态到Paused状态的过程是异步的,所以状态改变时立即调用isPlaying来判断可能不准确。
    • 从Paused状态恢复到播放状态是,其位置是暂停时的位置,start调用成功后,播放器将恢复至Started状态;
    • Paused状态下再调用pause对播放器状态无影响;
  • 调用stop()可以让播放器从Started、Paused、Prepared和PlaybackCompleted状态切换至Stopped状态;
    • Stopped状态下只有调用prepare和prepareAsync成功后进入Prepared状态才可以再次进入Started状态;
    • Stopped状态下再调用stop无影响;
  • 可以通过seekTo来调整播放位置:
    • 虽然异步调用seekTo能很快的返回,但实际的seek操作会花费一定时间,尤其是在线的音视频流情况下。当seek真正完成时,注册的OnSeekComplete.onSeekComplete()会被调用;
    • Prepared、Paused和PlaybackCompleted状态下也可以调用seekTo方法;
    • 可以通过getCurrentPosition来获取当前播放的位置;
  • 当播放到音视频流的结尾时,播放就完成了,此时:
    • 如果是循环模式,播放器就仍处于Started状态;
    • 如果是非循环模式,播放器就调用注册的OnCompletion.onCompletion()回调,并进入PlaybackCompleted状态;
    • 播放完成后调用start可以从头播放

合法与非法的状态

MediaPlayer的成员方法大多都有属于其合法的调用状态和非法的调用状态,这个可以通过上面的状态机来查看,也可以查看官方提供的表格。如下:

Method Name Valid States Invalid States Comments
attachAuxEffect {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} {Idle, Error} This method must be called after setDataSource. Calling it does not change the object state.
getAudioSessionId any {} This method can be called in any state and calling it does not change the object state.
getCurrentPosition {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} {Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
getDuration {Prepared, Started, Paused, Stopped, PlaybackCompleted} {Idle, Initialized, Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
getVideoHeight {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} {Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
getVideoWidth {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} {Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
isPlaying {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} {Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
pause {Started, Paused, PlaybackCompleted} {Idle, Initialized, Prepared, Stopped, Error} Successful invoke of this method in a valid state transfers the object to the Paused state. Calling this method in an invalid state transfers the object to the Error state.
prepare {Initialized, Stopped} {Idle, Prepared, Started, Paused, PlaybackCompleted, Error} Successful invoke of this method in a valid state transfers the object to the Prepared state. Calling this method in an invalid state throws an IllegalStateException.
prepareAsync {Initialized, Stopped} {Idle, Prepared, Started, Paused, PlaybackCompleted, Error} Successful invoke of this method in a valid state transfers the object to the Preparing state. Calling this method in an invalid state throws an IllegalStateException.
release any {} After release(), the object is no longer available.
reset {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} {} After reset(), the object is like being just created.
seekTo {Prepared, Started, Paused, PlaybackCompleted} {Idle, Initialized, Stopped, Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
setAudioAttributes {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} {Error} Successful invoke of this method does not change the state. In order for the target audio attributes type to become effective, this method must be called before prepare() or prepareAsync().
setAudioSessionId {Idle} {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} This method must be called in idle state as the audio session ID must be known before calling setDataSource. Calling it does not change the object state.
setAudioStreamType (deprecated) {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} {Error} Successful invoke of this method does not change the state. In order for the target audio stream type to become effective, this method must be called before prepare() or prepareAsync().
setAuxEffectSendLevel any {} Calling this method does not change the object state.
setDataSource {Idle} {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} Successful invoke of this method in a valid state transfers the object to the Initialized state. Calling this method in an invalid state throws an IllegalStateException.
setDisplay any {} This method can be called in any state and calling it does not change the object state.
setSurface any {} This method can be called in any state and calling it does not change the object state.
setVideoScalingMode {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} {Idle, Error} Successful invoke of this method does not change the state.
setLooping {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} {Error} Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state.
isLooping any {} This method can be called in any state and calling it does not change the object state.
setOnBufferingUpdateListener any {} This method can be called in any state and calling it does not change the object state.
setOnCompletionListener any {} This method can be called in any state and calling it does not change the object state.
setOnErrorListener any {} This method can be called in any state and calling it does not change the object state.
setOnPreparedListener any {} This method can be called in any state and calling it does not change the object state.
setOnSeekCompleteListener any {} This method can be called in any state and calling it does not change the object state.
setPlaybackParams {Initialized, Prepared, Started, Paused, PlaybackCompleted, Error} {Idle, Stopped} This method will change state in some cases, depending on when it’s called.
setScreenOnWhilePlaying any {} This method can be called in any state and calling it does not change the object state.
setVolume {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} {Error} Successful invoke of this method does not change the state.
setWakeMode any {} This method can be called in any state and calling it does not change the object state.
start {Prepared, Started, Paused, PlaybackCompleted} {Idle, Initialized, Stopped, Error} Successful invoke of this method in a valid state transfers the object to the Started state. Calling this method in an invalid state transfers the object to the Error state.
stop {Prepared, Started, Stopped, Paused, PlaybackCompleted} {Idle, Initialized, Error} Successful invoke of this method in a valid state transfers the object to the Stopped state. Calling this method in an invalid state transfers the object to the Error state.
getTrackInfo {Prepared, Started, Stopped, Paused, PlaybackCompleted} {Idle, Initialized, Error} Successful invoke of this method does not change the state.
addTimedTextSource {Prepared, Started, Stopped, Paused, PlaybackCompleted} {Idle, Initialized, Error} Successful invoke of this method does not change the state.
selectTrack {Prepared, Started, Stopped, Paused, PlaybackCompleted} {Idle, Initialized, Error} Successful invoke of this method does not change the state.
deselectTrack {Prepared, Started, Stopped, Paused, PlaybackCompleted} {Idle, Initialized, Error} Successful invoke of this method does not change the state.

权限相关

媒体播放需要的权限根据其实现的功能主要有以下三个:

  • 播放网络媒体需要INTERNET权限;
  • 播放时保持屏幕常亮需要WAKE_LOCK权限;
  • 实现播放缓存功能可能需要读写SD卡权限;

回调处理

MediaPlayer提供了比较多的回调监听处理,具体有:

  • setOnPreparedListener,播放器准备完毕;
  • setOnVideoSizeChangedListener,播放器尺寸改变;
  • setOnSeekCompleteListener,拖动完成;
  • setOnCompletionListener,播放完成;
  • setOnBufferingUpdateListener,缓存更新;
  • setOnInfoListener,有可用信息或警告时;
  • setOnErrorListener,发生错误时;

MediaPlayer需要在拥有自己Looper的线程中创建。

开发指南

Android的多媒体框架支持播放大多数媒体类型,可以很方便的在应用中整合多媒体播放功能。

基础部分

Android多媒体框架有两个主要的类:MediaPlayer和AudioManager,前者主要负责音视频播放的基础API,后者主要管理设备上的音频资源和音频输出。

权限相关

上面已经提到了,主要需要以下几种权限:

  • 网络权限,播放网络资源时需要;
  • WAKE_LOCK权限,保持屏幕常亮时需要(为了防止电量消耗,在不需要是请记得取消屏幕常亮的设置)
  • 存储权限,缓存功能需要

MediaPlayer的使用

可以用来播放以下三种类型:

  • Local Resource,即raw目录下的资源;
  • 本机资源,即通过ContentResolver获取到的手机上存在的音视频资源;
  • 网络资源,即网络音视频流;

播放raw资源

1
2
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // 上面讲了,通过create得到的MediaPlayer实例无需prepare

播放本机资源

1
2
3
4
5
6
Uri myUri = ....; // 获取本机资源的Uri
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

播放网络资源

1
2
3
4
5
6
String url = "http://........"; // 网络资源地址
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // 这里由于缓冲可能需要的时间比较久
mediaPlayer.start();

注意:播放的网络资源必须是可以逐步下载的。同时要捕获IllegalArgumentException和IOException因为请求的网络资源可能是不存在的。

异步资源准备

原则上可以直接使用MediaPlayer进行媒体播放,但是考虑到资源的准备可能花费较长时间,如果直接在UI线程调用,会阻塞UI,导致ANR的产生。所以我们在子线程中进行资源的准备,在准备好后发出通知即可。虽然我们可以自己进行相关的线程切换管理,但是MediaPlayer已经提供了已由的Api给我们调用,我们可以通过prepareAsync()来准备,然后再OnPreparedListener的onPrepared()方法中进行处理。OnPreparedListener可以通过setOnPreparedListener来设置。

状态管理

MediaPlayer是基于状态类的,所以在编码时要十分注意状态管理,通常情况下状态是按如下节奏变化的:
Idle->Initialized(->Preparing)->Prepared->Started(->Paused->PlaybackCompleted)->Stopped,等,状态的变化都是有相应的方法驱动的。

资源释放

由于MediaPlayer及其消耗系统资源,在不再使用它是请及时进行资源的释放。

1
2
mediaPlayer.release();
mediaPlayer = null;

在Service中使用MediaPlayer

如果需要支持后台播放的功能,那么就要使用Service进行MediaPlayer的播放操作。官方建议使用MediaBrowserServiceCompat和MediaBrowserCompat来实现,它是一种C/S架构。通常是一个持有MediaPlayer的Service实例来进行播放的。

异步执行

即使是后台播放,因为Service通常和Activity一样默认运行在主线程的,所以Service中同样也不能进行耗时操作。所以Service播放时同样要采用异步,并在准备完成后通知播放器进行播放。通常实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyService extends Service implements MediaPlayer.OnPreparedListener {
private static final String ACTION_PLAY = "com.example.action.PLAY";
MediaPlayer mediaPlayer = null;

public int onStartCommand(Intent intent, int flags, int startId) {
...
if (intent.getAction().equals(ACTION_PLAY)) {
mediaPlayer = ... // 初始化
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.prepareAsync(); // 异步prepare
}
}

/** 准备好后开始播放 */
public void onPrepared(MediaPlayer player) {
player.start();
}
}

异步过程中的错误处理

同步的播放操作在发生错误或异常后马上就会得到通知,但异步的情形是就需要我们自己在错误回调里面进行处理了,在Service里处理异步错误的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService extends Service implements MediaPlayer.OnErrorListener {
MediaPlayer mediaPlayer;

public void initMediaPlayer() {
// 初始化媒体播放器
// 设置错误监听
mediaPlayer.setOnErrorListener(this);
}

@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 错误后的处理
// 播放器已经进入错误的状态,需要重置后才能继续使用
}
}

WakeLock处理

WakeLock说明
后台播放之所以需要请求Wakelock,是因为当设备休眠是,系统为了省电,通常会关闭哪些不必要的服务,如CPU、WiFi,然而我们播放的时候是不希望这种情况发生的,所以我们就要自己处理下WakeLock,MediaPlayer处理WakeLock很简单,代码如下:

1
2
3
mediaPlayer = new MediaPlayer();
// ... other initialization here ...
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

播放网络资源时,要防止WiFi被关,请求WiFi的WakeLock代码如下:

1
2
3
4
5
6
7
8
// 请求WiFi 的WakeLock
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

wifiLock.acquire();

// 在合适的时机释放WakeLock
wifiLock.release();

注意我们在不必要的时候就要及时释放WakeLock,毕竟WakeLock会减少设备电池的寿命。

资源清理

前面已经提到了,MediaPlayer会消耗大量系统资源,所以我们在不再使用时要及时进行资源的释放,在后台播放的情形下,通常用如下的方式来进行资源的清理:

1
2
3
4
5
6
7
8
9
10
public class MyService extends Service {
MediaPlayer mediaPlayer;
// ...

@Override
public void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) mediaPlayer.release();
}
}

DRM管理

即Digital Right Management,数字版权管理,Android 8.0(API 26)提供了支持播放拥有数字版本的多媒体文件的Api,它与MediaDrm相似,但更抽象,且只实现了MediaDrm的部分功能。下面的代码片段展示了同步使用DRM MediaPlayer的一般过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setDataSource();
setOnDrmConfigHelper(); // 可选项,用于个性化配置
prepare(); // 资源准备(实际使用时这里是异步调用的)
if (getDrmInfo() != null) { // 获取DRM信息,如果不为空就处理
prepareDrm(); // 准备drm信息(这个过程通常需要异步处理,避免阻塞,通过OnDrmPreparedListener获取回调通知)
getKeyRequest(); // 获取不透明的密钥请求字节数组以发送到许可证服务器
provideKeyResponse(); // 将获取到的密钥响应通知到DRM引擎
}

// MediaPlayer is now ready to use
start();
// ...play/pause/resume...
stop();
releaseDrm();

异步初始化DRM信息

通过注册OnDrmInfoListener和OnDrmPreparedListener来获取异步调用完毕后的回调,然后再相应的回调中进行后续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setOnPreparedListener();
setOnDrmInfoListener();
setDataSource();
prepareAsync();
// ...

// 如果是受版权保护的内容,在这个回调方法里面进行处理
onDrmInfo() {
prepareDrm();
getKeyRequest();
provideKeyResponse();
}

// 当prepareAsync()结束后,将收到onPrepared()回调通知,如果是一个DRM内容,onDrmInfo()会优先于onPrepared()被调用,所以这里可以开始播放了
onPrepared() {

start();
}

加密资源处理

Android 8.0(API 26)MediaPlayer开始部分支持解密Common Encryption Scheme(CENC)、H.264(METHOD=SAMPLE-AES)和AAC,之前就支持Full-segment encrypted media (METHOD=AES-128)。

支持不完善,一般不采用。

总结

本文主要介绍了Android官方播放器MediaPlayer的一般使用方法及使用的一些注意事项,还对一些常见的播放场景做了说明,对刚接触android多媒体开发的还是是否有帮助的。

<i class="fa fa-angle-left" aria-label="上一页"></i>1…131415…22<i class="fa fa-angle-right" aria-label="下一页"></i>

216 日志
43 分类
43 标签
GitHub
© 2025 Randy Zhang
由 Hexo 强力驱动
|
主题 — NexT.Gemini v6.1.0