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

相关文章

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Oracle的to_date()函数详解

《Oracle的to_date()函数详解》Oracle的to_date()函数用于日期格式转换,需要注意Oracle中不区分大小写的MM和mm格式代码,应使用mi代替分钟,此外,Oracle还支持毫... 目录oracle的to_date()函数一.在使用Oracle的to_date函数来做日期转换二.日

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

Spring中Bean有关NullPointerException异常的原因分析

《Spring中Bean有关NullPointerException异常的原因分析》在Spring中使用@Autowired注解注入的bean不能在静态上下文中访问,否则会导致NullPointerE... 目录Spring中Bean有关NullPointerException异常的原因问题描述解决方案总结

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

python-nmap实现python利用nmap进行扫描分析

《python-nmap实现python利用nmap进行扫描分析》Nmap是一个非常用的网络/端口扫描工具,如果想将nmap集成进你的工具里,可以使用python-nmap这个python库,它提供了... 目录前言python-nmap的基本使用PortScanner扫描PortScannerAsync异