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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:https://blog.csdn.net/achina2011jy/article/details/112800156
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/267850

相关文章

PostgreSQL中rank()窗口函数实用指南与示例

《PostgreSQL中rank()窗口函数实用指南与示例》在数据分析和数据库管理中,经常需要对数据进行排名操作,PostgreSQL提供了强大的窗口函数rank(),可以方便地对结果集中的行进行排名... 目录一、rank()函数简介二、基础示例:部门内员工薪资排名示例数据排名查询三、高级应用示例1. 每

全面掌握 SQL 中的 DATEDIFF函数及用法最佳实践

《全面掌握SQL中的DATEDIFF函数及用法最佳实践》本文解析DATEDIFF在不同数据库中的差异,强调其边界计算原理,探讨应用场景及陷阱,推荐根据需求选择TIMESTAMPDIFF或inte... 目录1. 核心概念:DATEDIFF 究竟在计算什么?2. 主流数据库中的 DATEDIFF 实现2.1

MySQL中的LENGTH()函数用法详解与实例分析

《MySQL中的LENGTH()函数用法详解与实例分析》MySQLLENGTH()函数用于计算字符串的字节长度,区别于CHAR_LENGTH()的字符长度,适用于多字节字符集(如UTF-8)的数据验证... 目录1. LENGTH()函数的基本语法2. LENGTH()函数的返回值2.1 示例1:计算字符串

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

MySQL 中的 CAST 函数详解及常见用法

《MySQL中的CAST函数详解及常见用法》CAST函数是MySQL中用于数据类型转换的重要函数,它允许你将一个值从一种数据类型转换为另一种数据类型,本文给大家介绍MySQL中的CAST... 目录mysql 中的 CAST 函数详解一、基本语法二、支持的数据类型三、常见用法示例1. 字符串转数字2. 数字

Python内置函数之classmethod函数使用详解

《Python内置函数之classmethod函数使用详解》:本文主要介绍Python内置函数之classmethod函数使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 类方法定义与基本语法2. 类方法 vs 实例方法 vs 静态方法3. 核心特性与用法(1编程客

Python函数作用域示例详解

《Python函数作用域示例详解》本文介绍了Python中的LEGB作用域规则,详细解析了变量查找的四个层级,通过具体代码示例,展示了各层级的变量访问规则和特性,对python函数作用域相关知识感兴趣... 目录一、LEGB 规则二、作用域实例2.1 局部作用域(Local)2.2 闭包作用域(Enclos

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

MySQL count()聚合函数详解

《MySQLcount()聚合函数详解》MySQL中的COUNT()函数,它是SQL中最常用的聚合函数之一,用于计算表中符合特定条件的行数,本文给大家介绍MySQLcount()聚合函数,感兴趣的朋... 目录核心功能语法形式重要特性与行为如何选择使用哪种形式?总结深入剖析一下 mysql 中的 COUNT

MySQL 中 ROW_NUMBER() 函数最佳实践

《MySQL中ROW_NUMBER()函数最佳实践》MySQL中ROW_NUMBER()函数,作为窗口函数为每行分配唯一连续序号,区别于RANK()和DENSE_RANK(),特别适合分页、去重... 目录mysql 中 ROW_NUMBER() 函数详解一、基础语法二、核心特点三、典型应用场景1. 数据分