ExoPlayer播放器剖析(四)从renderer.render函数分析至MediaCodec

本文主要是介绍ExoPlayer播放器剖析(四)从renderer.render函数分析至MediaCodec,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

关联博客

ExoPlayer播放器剖析(一)进入ExoPlayer的世界
ExoPlayer播放器剖析(二)编写exoplayer的demo
ExoPlayer播放器剖析(三)流程分析—从build到prepare看ExoPlayer的创建流程
ExoPlayer播放器剖析(四)从renderer.render函数分析至MediaCodec
ExoPlayer播放器剖析(五)ExoPlayer对AudioTrack的操作
ExoPlayer播放器剖析(六)ExoPlayer同步机制分析
ExoPlayer播放器剖析(七)ExoPlayer对音频时间戳的处理
ExoPlayer播放器扩展(一)DASH流与HLS流简介

一、引言:
在上一篇博客中,我们对exoplayer的流程做了一个分析,可以看到,exoplayer的初始化流程步骤并不复杂,而往往api越简单的接口,其下面的实现越麻烦,这篇博客,我们就针对上一篇末尾提到的doSomeWork这个关键函数入手,看看exoplayer是如何封装至MediaCodec的。

二、doSomeWork函数分析:
首先贴出doSomeWork的中的关键代码:

  private void doSomeWork() throws ExoPlaybackException, IOException {.../* 1.更新音频时间戳 */updatePlaybackPositions();.../* 2.调用各个类型的render进行数据处理 */for (int i = 0; i < renderers.length; i++) {.../* 核心处理方法 */renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);  ....}...if (finishedRendering && playingPeriodHolder.info.isFinal) {setState(Player.STATE_ENDED);stopRenderers();/* 更新播放器状态为STATE_READY */} else if (playbackInfo.playbackState == Player.STATE_BUFFERING&& shouldTransitionToReadyState(renderersAllowPlayback)) {setState(Player.STATE_READY);/* 如果playWhenReady为true,则开始渲染 */if (shouldPlayWhenReady()) {startRenderers();}} else if (playbackInfo.playbackState == Player.STATE_READY&& !(enabledRendererCount == 0 ? isTimelineReady() : renderersAllowPlayback)) {rebuffering = shouldPlayWhenReady();setState(Player.STATE_BUFFERING);stopRenderers();}...if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)|| playbackInfo.playbackState == Player.STATE_BUFFERING) {/* 开启渲染之后将进入这个分支:ACTIVE_INTERVAL_MS为10ms */maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS);} else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);} else {handler.removeMessages(MSG_DO_SOME_WORK);}
}

1.renderer.render函数分析:
先贴出代码:

----------------------------------------------------------------------------
render@E:\GitHub_Windows\ExoPlayer\library\core\src\main\java\com\google\android\exoplayer2\mediacodec\MediaCodecRenderer.java
----------------------------------------------------------------------------@Overridepublic void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {/* 是否处理EOS */if (pendingOutputEndOfStream) {pendingOutputEndOfStream = false;processEndOfStream();}if (pendingPlaybackException != null) {ExoPlaybackException playbackException = pendingPlaybackException;pendingPlaybackException = null;throw playbackException;}try {if (outputStreamEnded) {renderToEndOfStream();return;}if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) {// We still don't have a format and can't make progress without one.return;}// We have a format./* 配置codec */maybeInitCodecOrBypass();if (bypassEnabled) {TraceUtil.beginSection("bypassRender");while (bypassRender(positionUs, elapsedRealtimeUs)) {}TraceUtil.endSection();} else if (codec != null) {long renderStartTimeMs = SystemClock.elapsedRealtime();TraceUtil.beginSection("drainAndFeed");/* 消耗解码数据 */while (drainOutputBuffer(positionUs, elapsedRealtimeUs)&& shouldContinueRendering(renderStartTimeMs)) {}/* 填充源数据 */while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {}TraceUtil.endSection();} else {decoderCounters.skippedInputBufferCount += skipSource(positionUs);// We need to read any format changes despite not having a codec so that drmSession can be// updated, and so that we have the most recent format should the codec be initialized. We// may also reach the end of the stream. Note that readSource will not read a sample into a// flags-only buffer.readToFlagsOnlyBuffer(/* requireFormat= */ false);}decoderCounters.ensureUpdated();} catch (IllegalStateException e) {if (isMediaCodecException(e)) {throw createRendererException(createDecoderException(e, getCodecInfo()), inputFormat);}throw e;}}

整个render函数其实就干了三件事:调用子类MediaCodecVideoRenderer和
MediaCodecAudioRenderer去配置codec、消耗MediaCodec中解码处理的数据和往MediaCodec中填充源数据。
着重看配置codec的函数maybeInitCodecOrBypass():

protected final void maybeInitCodecOrBypass() throws ExoPlaybackException 
{...while (codec == null) {MediaCodecInfo codecInfo = availableCodecInfos.peekFirst();if (!shouldInitCodec(codecInfo)) {return;}try {/* 初始化codec */initCodec(codecInfo, crypto);} catch (Exception e) {...}}availableCodecInfos = null;}

继续压缩代码,只关注initCodec(终于可以看到MediaCodecle了~):

  private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception {long codecInitializingTimestamp;long codecInitializedTimestamp;MediaCodec codec = null;String codecName = codecInfo.name;float codecOperatingRate =Util.SDK_INT < 23? CODEC_OPERATING_RATE_UNSET: getCodecOperatingRateV23(operatingRate, inputFormat, getStreamFormats());if (codecOperatingRate <= assumedMinimumCodecOperatingRate) {codecOperatingRate = CODEC_OPERATING_RATE_UNSET;}/* 创建了codec适配器来管理codec */MediaCodecAdapter codecAdapter = null;try {codecInitializingTimestamp = SystemClock.elapsedRealtime();TraceUtil.beginSection("createCodec:" + codecName);/* 实例化MediaCodec */codec = MediaCodec.createByCodecName(codecName);if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD&& Util.SDK_INT >= 23) {codecAdapter = new AsynchronousMediaCodecAdapter(codec, getTrackType());} else if (mediaCodecOperationMode== OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING&& Util.SDK_INT >= 23) {codecAdapter =new AsynchronousMediaCodecAdapter(codec, /* enableAsynchronousQueueing= */ true, getTrackType());} else {/* 同步模式请看这里 */codecAdapter = new SynchronousMediaCodecAdapter(codec);}TraceUtil.endSection();TraceUtil.beginSection("configureCodec");/* 配置codec */configureCodec(codecInfo, codecAdapter, inputFormat, crypto, codecOperatingRate);TraceUtil.endSection();TraceUtil.beginSection("startCodec");/* 开启解码 */codecAdapter.start();TraceUtil.endSection();codecInitializedTimestamp = SystemClock.elapsedRealtime();/* 获取buffer数组:Android 5.0以下 */getCodecBuffers(codec);} catch (Exception e) {if (codecAdapter != null) {codecAdapter.shutdown();}if (codec != null) {resetCodecBuffers();codec.release();}throw e;}this.codec = codec;this.codecAdapter = codecAdapter;this.codecInfo = codecInfo;this.codecOperatingRate = codecOperatingRate;codecInputFormat = inputFormat;codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName);codecNeedsDiscardToSpsWorkaround =codecNeedsDiscardToSpsWorkaround(codecName, codecInputFormat);codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName);codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);codecNeedsMonoChannelCountWorkaround =codecNeedsMonoChannelCountWorkaround(codecName, codecInputFormat);codecNeedsEosPropagation =codecNeedsEosPropagationWorkaround(codecInfo) || getCodecNeedsEosPropagation();if ("c2.android.mp3.decoder".equals(codecInfo.name)) {c2Mp3TimestampTracker = new C2Mp3TimestampTracker();}if (getState() == STATE_STARTED) {codecHotswapDeadlineMs = SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS;}decoderCounters.decoderInitCount++;/* 计算初始化codec的耗时 */long elapsed = codecInitializedTimestamp - codecInitializingTimestamp;onCodecInitialized(codecName, codecInitializedTimestamp, elapsed);}

拨开层层面纱,总算看到了MediaCodec,exoplayer在对codec的管理一块,引入了适配器的概念,至于同步方式和异步方式,对MediaCodec的使用逻辑上是不一样的,通常,我们是以同步的方式来进行,config和start是对MediaCodec的标准操作逻辑,最后一步会去获取输入输出的buffer数组(看了下代码,只有Android 5.0以下才会走这一步),这些都是由MediaCodec底层来提供的。另外,代码中还非常有意思的一点是,统计了初始化codec的总耗时。继续往下分析,我们先看下同步模式做了什么:

  public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) {this.codec = mediaCodec;}

仅仅是codec的一个赋值,再看下configureCodec(这里以音频为例):

  @Overrideprotected void configureCodec(MediaCodecInfo codecInfo,MediaCodecAdapter codecAdapter,Format format,@Nullable MediaCrypto crypto,float codecOperatingRate) {codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats());codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name);MediaFormat mediaFormat =getMediaFormat(format, codecInfo.codecMimeType, codecMaxInputSize, codecOperatingRate);/* 调用适配器的configure */codecAdapter.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);// Store the input MIME type if we're only using the codec for decryption.boolean decryptOnlyCodecEnabled =MimeTypes.AUDIO_RAW.equals(codecInfo.mimeType)&& !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType);decryptOnlyCodecFormat = decryptOnlyCodecEnabled ? format : null;}

核心还是调用适配器的configure,前面我们说过对应创建的是同步适配器,所以看一下SynchronousMediaCodecAdapter中的configure函数:

  @Overridepublic void configure(@Nullable MediaFormat mediaFormat,@Nullable Surface surface,@Nullable MediaCrypto crypto,int flags) {codec.configure(mediaFormat, surface, crypto, flags);}

完全是调用Android的MediaCodec接口了,同理,start函数也是一样:

  @Overridepublic void start() {codec.start();}

2.分析startRenderers:
我们看startRenderers方法里面做了什么:

  private void startRenderers() throws ExoPlaybackException {rebuffering = false;/* 开启MediaClock */mediaClock.start();for (Renderer renderer : renderers) {/* 如果render使能 */if (isRendererEnabled(renderer)) {renderer.start();}}}

MediaClock是exoplayer定义的一个用于记录当前系统时间的一个类,后续在同步过程会扮演非常重要的角色,如果render被使能的话,那么将调用start接口,renderer对应的类是Renderer,我们看下Renderer接口的继承关系:

public interface Renderer extends PlayerMessage.Target {...
}

继承自Renderer的类有:
在这里插入图片描述

看这里是不是有种熟悉的感觉?在上一篇博客中有讲过SimpleExoPlayer的构造函数中会去创建5~6种render,所以,对应的实现类就是这是这个地方,对于解码最重要的两个render,自然就是MediaCodecAudioRenderer类和MediaCodecVideoRenderer类了。
先看下BaseRenderer中start的实现:

  @Overridepublic final void start() throws ExoPlaybackException {/* 设置Renderer状态为STATE_ENABLED */Assertions.checkState(state == STATE_ENABLED);state = STATE_STARTED;onStarted();}

onStarted()就是各个类来具体实现了,先看MediaCodecAudioRenderer:

  @Overrideprotected void onStarted() {/* 调用父类onStarted,而父类该函数do nothing */super.onStarted();audioSink.play();}

这里的audioSink实际上是exoplayer基于AudioTrack封装的类,它的实现是在DefaultAudioSink中:

----------------------------------------------------------------------------
play@ExoPlayer\library\core\src\main\java\com\google\android\exoplayer2\audio\DefaultAudioSink.java
----------------------------------------------------------------------------@Overridepublic void play() {playing = true;if (isAudioTrackInitialized()) {audioTrackPositionTracker.start();audioTrack.play();}}

可以看到,如果AudioTrack已经创建,那么就直接调用AudioTrack的play方法进行音频数据的播放。
再回到前面去看MediaCodecVideoRenderer中onStarted()方法的实现:

----------------------------------------------------------------------------
onStarted@ExoPlayer\library\core\src\main\java\com\google\android\exoplayer2\video\MediaCodecVideoRenderer.java
----------------------------------------------------------------------------@Overrideprotected void onStarted() {super.onStarted();/* 丢帧数清0 */droppedFrames = 0;droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();/* 将上一帧渲染时间设定为系统启动后至今的时间,即当前时间 */lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;totalVideoFrameProcessingOffsetUs = 0;videoFrameProcessingOffsetCount = 0;/* 更新帧率 */updateSurfaceFrameRate(/* isNewSurface= */ false);}

updateSurfaceFrameRate函数在AndroidR(Android 11)版本以下将直接返回,所以,这个函数做的事不多,主要是清零丢帧数及记录当前的系统时间。

总结:
startRenderers()方法对音频而言是开始往audiotrack中写数据进行播放,对视频而言是记录当前的系统时间为后面的渲染做准备。

三、总结:
在这里插入图片描述

对MediaCodec的配置过程就分析完了,doSomeWork每次循环都会去调用具体的render类进行处理,在第一次调用的时候,会根据对应的mime type去初始化codec,在最下层的适配器中会去调用Android原生的MediaCodec接口,之后就会去不停地去生产和消耗解码后的数据了。

这篇关于ExoPlayer播放器剖析(四)从renderer.render函数分析至MediaCodec的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/267850

相关文章

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

Python中isinstance()函数原理解释及详细用法示例

《Python中isinstance()函数原理解释及详细用法示例》isinstance()是Python内置的一个非常有用的函数,用于检查一个对象是否属于指定的类型或类型元组中的某一个类型,它是Py... 目录python中isinstance()函数原理解释及详细用法指南一、isinstance()函数

python中的高阶函数示例详解

《python中的高阶函数示例详解》在Python中,高阶函数是指接受函数作为参数或返回函数作为结果的函数,下面:本文主要介绍python中高阶函数的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录1.定义2.map函数3.filter函数4.reduce函数5.sorted函数6.自定义高阶函数

Python中的sort方法、sorted函数与lambda表达式及用法详解

《Python中的sort方法、sorted函数与lambda表达式及用法详解》文章对比了Python中list.sort()与sorted()函数的区别,指出sort()原地排序返回None,sor... 目录1. sort()方法1.1 sort()方法1.2 基本语法和参数A. reverse参数B.

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe