Android音视频开发简介

  • Post author:
  • Post category:其他




音视频开发文档



媒体应用概览

对于音视频开发来说,总共就包括两部分:播放器与界面。播放器比较容易理解,但界面可能对音频来说不需要。此处界面不仅仅包括显示视频的那个界面,而且包括显示播放器状态的传输控件,并且它有用于运行播放器。



播放器

  • 在Android中,你可以从零开始构建自己的播放器(大佬专属),也可以从以下选项中进行选择。

    • MediaPlayer 类提供准系统播放器的基本功能,支持最常见的音频/视频格式和数据源。
    • ExoPlayer是提供了低层级Android音频API的开源库。ExoPlayer支持DASH和HLS流等高性能功能,这些功能在MediaPlayer中未提供。你可以自定义ExoPlayer代码,从而轻松添加新组件。ExoPlayer只能用于Android 4.1 及更高版本。

      媒体会话和媒体控制器

      界面和播放器我们可以任意选择,但对于所有媒体播放器来说,这两部分之间的交互的性质都基本相同。Android框架定义了两个类(Media Session和Media Controller)



MediaSession

  • 媒体会话负责与播放器的所有通讯。系统只能从控制播放器的媒体会话中调用播放器。
  • 会话可以维护播放器状态以及播放内容相关信息。会话可以接收来自一个或多个媒体播放器的回调。但响应回调的逻辑必须保持一致。



MediaController

  • 媒体控制器会隔离你的界面。界面代码只与媒体控制器通信。每当会话状态发生变化时,它也会接收来自MediaSession的回调。这提供了一种自动更新关联界面的机制。MediaController一次只能连接到一个MediaSession。

    media-compat库

    media-compat 库包含可帮助构建音视频应用的类。这些类与运行Android2.3 及更高版本的设备兼容。

    MediaSession 和MediaController 建议实现方式是MediaSessionCompat 和MediaControllerCompat 类。他们取代了Android 5.0 中引入的早期版本MediaSession 和 MediaController类。compat更加便捷轻松。

    衡量性能

    在Android 8.0 及更高版本中,getMetrics() 方法可用于某些媒体类。它会返回一个包含配置和性能信息的 PersistableBundle 对象。



MediaPlayer使用

需要使用权限声明

// 网络权限声明    
 <uses-permission android:name="android.permission.INTERNET" />          
// 唤醒锁定权限    
 <uses-permission android:name="android.permission.WAKE_LOCK" />        

使用方法

MediaPlayer 类是媒体框架的重要组成部分之一。此类的对象能够获取、解码以及播放音视频,而且只需要少量的设置。它支持多种不同的媒体源,例如:

  • 本地资源
  • 内部URI,例如你可能从内容解析器那获取的URI
  • 外部网址(流式传输)

    代码展示

    本地原始资源



    播放本地可用的URL



    通过HTTP流式传输



异步准备

原则上,使用MediaPlayer很简单。但还需要执行一些额外操作。例如,对prepare()的调用可能需要很长时间执行,因为它可能涉及获取和解码媒体数据。因此一般不能在主线程调用它。否则会卡顿甚至ANR错误。

为了避免这种情况,我们可以自行编写县城逻辑,也可以使用prepareAsync()方法。当媒体准备就绪后,系统会调用 setOnPreparedListener() 配置的 MediaPlayer.OnPreparedListener 的 onPrepared() 方法。

管理状态

MediaPlayer 以状态为基础。也就是说,MediaPlayer 具有内部状态,编写代码时必须注意,某些操作仅在播放器处于特定状态时才有效。如果你在错误的状态下执行某项操作,则会抛出异常。

  • 当MediaPlayer新创建或者调用 reset()的时候,它处于idle状态 。并且在release() 被调用之后,它处于End 状态。在这两种状态之间是MediaPlayer对象的生命周期。

    • 新构造的MediaPlayer跟reset()之后的MediaPlayer还是有些细微区别。在一些编程错误调用方法后,新构造的对象不会为用户回调方法OnErrorListener.onError(),并且对象状态保持不变;但是如果这些方法在之后被调用reset(),用户提供的回调方法OnErrorListener.onError()将由内部播放器引擎调用,并且该对象将转移到Error状态。
    • 建议一旦不再使用MediaPlayer对象,请 release()立即调用
  • 只有在idel状态下可以使用 setDataSource()。其他状态下会抛出IllegalStateException。
  • MediaPlayer对象必须首先进入“准备”状态,然后才能开始播放。

    • 有同步和异步两种方式。

      • prepare()同步方法,一旦调用,状态就会变为prepared
      • prepareAsync()(异步方法)当调用返回后,对象状态也会变为prepared
    • 重要的是要注意,Preparing状态是一个过渡状态,并且在MediaPlayer对象处于Preparing状态时调用具有副作用的任何方法的行为是不确定的。
    • 如果在任何其他状态下调用 prepare()或 prepareAsync(),则抛出IllegalStateException
  • 要开始播放,start()必须调用。start()成功返回后,MediaPlayer 对象处于Started状态。isPlaying()可以调用它测试MediaPlayer 对象是否处于started状态。

    • 调用start() 对已经处于started状态的MediaPlayer对象无效。
  • 可以暂停和停止播放。通过调用pause(),将状态由started转为Paused。

    • 调用pause()对于已经处于Paused的无效。
  • 调用stop()将停止播放,并使处于Started,Paused,Prepared 或PlaybackCompleted状态的MediaPlayer进入 Stopped状态。

    • 调用stop()对于已经处于Stopped的无效。
  • 调用seekTo()将调整播放位置

    • 当回调完成的时候,会调用OnSeekComplete.onSeekComplete()
  • 当播放到达流的结尾时,播放完成

    • 如果setLooping(true)时,MediaPlayer会保持的Started状态。
    • 如果setLooping(false)时,如果通过预先注册了OnCompletionListener,则播放器引擎会调用用户提供的回调方法OnCompletion.onCompletion()。回调的调用表明该对象现在处于 PlaybackCompleted状态。
    • 在PlaybackCompleted 状态下,调用start()可以从音频/视频源的开头重新开始播放。



在Service中使用MediaPlayer

当希望在后台播放媒体内推的时候,就需要启动一个Service来控制MediaPlayer实例。你需要将MediaPlayer嵌入到MediaBrowserServiceCompat这个Service中,并使其在Activity中与MediaBrowserCompat进行互动。



异步运行

与Activity类似,Service默认在主线程完成,为了避免长时间的任务导致卡顿或者ANR,你可以自己实现异步执行,或使用框架的诸多工具进行异步处理。

在主线程中使用MediaPlayer时,调用prepareAsync()而非prepare(),并实现MediaPlayer.OnPreparedListener,以便在准备工作完成后获取通知并开始播放。例如:



处理异步错误

在同步操作中,系统通常会通过异常或错误代码来指示错误,但当您使用异步资源时,应确保以适当的方式向应用发出错误通知。对于 MediaPlayer,您可以实现 MediaPlayer.OnErrorListener ,来处理发出的错误通知:



使用唤醒锁定

在系统进入休眠时,如果还想让Service继续运行,这个时候需要使用唤醒锁定。它可以告诉系统:你的应用正在使用一些即使手机处于闲置状态也应该可用的功能。

需要进行一个初始化。完成后,处于播放状态时会锁定,在暂停或停止播放时释放锁定:



此示例只能保证CPU处于唤醒状态。如果使用WLAN并通过网络流式传输媒体内容,则也需要保持WifiLock,该锁定必须手动获取和释放。当开始使用网址准备MediaPlayer时,需要获取锁定。



当您暂停或停止媒体内容,或者当您不再需要网络时,应释放该锁定:



清理

MediaPlayer 对象会消耗大量的系统资源,因此当不需要使用的时候,需要清理。



数字版权管理(DRM)

从 Android 8.0(API 级别 26)开始,MediaPlayer 包含支持播放受 DRM 保护的资料的 API。这些 API 与由 MediaDrm 提供的低级别 API 类似,但前者是在较高级别运行,并且不会公开底层提取器、DRM 和加密对象。

尽管 MediaPlayer DRM API 并不提供 MediaDrm 的完整功能,但它支持最常见的使用情形。当前实现可以处理以下内容类型:

  • 受 Widevine 保护的本地媒体文件
  • 受 Widevine 保护的远程/流式传输媒体文件

    以下代码段演示了如何在简单的同步实现中使用新的 DRM MediaPlayer 方法。

    要管理受 DRM 控制的媒体,您需要在 MediaPlayer 调用的常规流程中包含新的方法,如下所示:



    首先,像往常一样初始化 MediaPlayer 对象并使用 setDataSource() 来设置其来源。然后,要使用 DRM,请执行以下步骤:
  1. 如果您希望应用执行自定义配置,请定义 OnDrmConfigHelper 接口,并使用 setOnDrmConfigHelper() 将其附加到播放器。
  2. 调用 prepare()。
  3. 调用 getDrmInfo()。如果来源具有 DRM 内容,则该方法会返回一个非 null MediaPlayer.DrmInfo 值。

    如果存在 MediaPlayer.DrmInfo:
  4. 检查可用 UUID 的映射,然后选择一个。
  5. 通过调用 prepareDrm() 为当前来源准备 DRM 配置。
  • 如果您创建并注册了 OnDrmConfigHelper 回调,则系统会在执行 prepareDrm()时调用该回调。这样一来,您就能够在打开 DRM 会话之前执行 DRM 属性的自定义配置。该回调会在名为 prepareDrm() 的线程中同步调用。要访问 DRM 属性,请调用 getDrmPropertyString()和 setDrmPropertyString()。避免执行冗长的操作。
  • 如果尚未配置设备, prepareDrm() 还会访问配置服务器来配置该设备。所需的时间因网络连接而有所不同。
  1. 要获取不透明的密钥请求字节数组以发送到许可服务器,请调用 getKeyRequest()。
  2. 要向 DRM 引擎告知从许可服务器接收到的密钥响应,请调用 provideKeyResponse()。结果取决于密钥请求的类型:
  • 如果响应针对的是离线密钥请求,则结果为密钥组标识符。您可以将此密钥组标识符与 restoreKeys() 结合使用,以将这些密钥恢复到新的会话中。
  • 如果响应针对的是流式传输或释放请求,则结果为 null。



第三方库音视频学习



VideoView

VideoView是继承自SurfaceView的。其实真正用于播放视频的是MediaPlayer+SurfaceView,VideoView只是把两者封装起来,便于开发者使用。



SurfaceView

Android中提供了View进行绘图处理,View可以满足大部分的绘图需求,但是有时候,View却显得力不从心,所以Android提供了SurfaceView给Android开发者。



为什么要使用SurfaceView

我们知道View是通过刷新来重新绘制,系统通过发出的VSSYNS信号来进行屏幕重绘,刷新的时间间隔是16ms,如果我们在16ms以内将绘制工作完成,就没有问题。但如果绘制过程逻辑复杂,并且界面更新非常频繁,就会造成界面卡顿,影响用户体验。SurfaceView就是来解决这一问题的。



View与SurfaceView区别

  1. View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面。
  2. View在主线程中对页面刷新,而SurfaceView则开启一个子线程来对页面进行刷新。
  3. View在绘图时没有实现双缓冲机制,SurfaceView在底层机制中实现了双缓冲机制。
  • 双缓冲技术介绍

    双缓冲技术是游戏开发中的一个重要的技术。当一个动画争先显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。​



SurfaceView使用

提供嵌入视图层次结构内部的专用绘图图面。你可以控制此表面的格式,也可以控制其大小;SurfaceView负责将表面放置咋子屏幕上的正确位置。

通过SurfaceHolder接口可以访问底层表面,可以通过调用进行检索getHolder()

当SurfaceView的窗口可见时,将创建Surface。可以实现SurfaceHolder.Callback来监听创建销毁和改变。

class MySurface
( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr), SurfaceHolder.Callback {  
 	private var mSurfaceHolder: SurfaceHolder = holder    
 	init {        mSurfaceHolder.addCallback(this)    
		}
		   
	override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int){  
 	}
 	    
	override fun surfaceDestroyed(holder: SurfaceHolder) {  
	}    
	
	override fun surfaceCreated(holder: SurfaceHolder) { 
	       holder.surface   
	}

}



VideoView流程

VideoView的功能比较简单,非常适用于那些只单纯地播放视频的场景,如不断播放的广告视频。不适用于交互性多的视频播放场景,像调节亮度,调节音量,双击暂停等交互逻辑。VideoView是无法实现的。

注意:进入后台时,VideoView不会保留其完整状态。特别是,它不会恢复当前的播放状态,播放位置。


简单使用

package com.example.mediademoktimport android.net.Uriimport 
android.os.Bundleimport android.widget.MediaControllerimport 
android.widget.VideoViewimport androidx.appcompat.app.AppCompatActivityclass 


MediaActivity : AppCompatActivity() {
	private lateinit var videoView: VideoView    private var currentPosition: Int = 0
	override fun onCreate(savedInstanceState: Bundle?) {        		
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_media)        
		videoView = findViewById<VideoView>(R.id.video_view)        
		val videoUri = Uri.parse( "android.resource://"   + packageName + "/"    + R.raw.vid_bigbuckbunny )       
		videoView.setVideoURI(videoUri)        
		videoView.start()        
		videoView.setMediaController(MediaController(this))    
    }    

    override fun onPause() {
		super.onPause()        
		currentPosition = videoView.currentPosition    
	}  
  
	override fun onResume() {        
		super.onResume()       
		 videoView.seekTo(currentPosition)        
		 videoView.resume()    
	 }
}



版权声明:本文为weixin_44241851原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。