ExoPlayer架构详解与源码分析(15)——Renderer
系列文章目录
ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor
ExoPlayer架构详解与源码分析(10)——H264Reader
ExoPlayer架构详解与源码分析(11)——DataSource
ExoPlayer架构详解与源码分析(12)——Cache
ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource
ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod
ExoPlayer架构详解与源码分析(15)——Renderer
文章目录
- 系列文章目录
- 前言
- Renderer
- BaseRenderer
- MediaCodec
- MediaCodecRenderer
- MediaCodecAudioRenderer
- MediaCodecVideoRenderer
- 参考时间戳的计算
- 总结
前言
如果你已经看完理解了前面MediaSource的内容,我相信你已经知道数据是如何获取并解析好放入到缓存了,我们先跳过中间那些控制管理环节,这些数据最终流入的方向就是本篇要讲的Renderer了。可以把Renderer想象成火箭的涡轮发动机,从MediaSource那源源不断的获取燃料,在发动机里点火燃烧,为火箭升空提供强大的动力。和火箭一样要想升空发动机必须平稳,再火箭运行的不通时间精确的执行预设好的动作。这就需要一个良好的时间同步设计,而Renderer的核心内容就是同步。
Renderer
渲染从SampleStream读取的媒体。
在内部,渲染器的生命周期由所属的ExoPlayer管理。随着整体播放状态和启用的轨道的变化,渲染器会在各种状态之间转换。有效的状态转换如下所示,并用每次转换期间调用的方法进行注释。
看下主要方法
- init 初始化Renderer,入参index为当前Renderer在所有Renderer中的索引,入参playerId为当前播放器的ID
- enable 使渲染器能够使用传入的SampleStream,当Renderer 处于Disabled状态是才可能被调用
- start 启动渲染器,这意味着对render的调用将导致媒体被渲染。当Renderer 处于Enable状态时才能调用此方法
- render 增量渲染SampleStream 。当渲染器处于以下Enable、 Started状态时可以调用此方法 。
- replaceStream 替换SampleStream 。当渲染器处于以下Enable、 Started状态时可以调用此方法 。
Renderer对象创建完成一般先调init方法初始化,然后调用enable传入SampleStream,enable内部会调用replaceStream初始化SampleStream,之后调用start将状态置为Started,最后调用render方法开始渲染
再看下Renderer模块的整体结构
Renderer直接由抽象类BaseRenderer实现,下面的MediaCodecRenderer(音视频)、TextRenderer(字幕)、MetadataRenderer(Meta信息)、CameraMotionRenderer(镜头信息,用于VR全景之类的数据)对应各种类型轨道的渲染器。本文篇幅有限,主要介绍音视频也就是MediaCodecRenderer,其他的Renderer感兴趣的可以自行研究。可以看到MediaCodecRenderer下又分视频Video和音频Audio两大块,视频最终交给Android系统的MediaCodec来处理,而音频最终交由Android系统的AudioTrack处理。
BaseRenderer
Renderer的直接实现类,主要用于一些状态的控制存储,和一些全局变量的管理
@Overridepublic final void init(int index, PlayerId playerId) {this.index = index;this.playerId = playerId;}@Overridepublic final void enable(RendererConfiguration configuration,//renderer配置信息Format[] formats,//轨道信息SampleStream stream,//待渲染数据long positionUs,//当前播放位置boolean joining,//是否启用此渲染器来加入正在进行的播放boolean mayRenderStartOfStream,//即使状态尚未STATE_STARTED ,是否允许此渲染器渲染流的开头。long startPositionUs,//渲染的开始位置long offsetUs)//在渲染之前添加到从stream读取的缓冲区时间戳的偏移量。throws ExoPlaybackException {Assertions.checkState(state == STATE_DISABLED);this.configuration = configuration;state = STATE_ENABLED;onEnabled(joining, mayRenderStartOfStream);//调用子类replaceStream(formats, stream, startPositionUs, offsetUs);resetPosition(positionUs, joining);}@Overridepublic final void replaceStream(Format[] formats, SampleStream stream, long startPositionUs, long offsetUs)throws ExoPlaybackException {Assertions.checkState(!streamIsFinal);this.stream = stream;//替换当前的全局SampleSteamif (readingPositionUs == C.TIME_END_OF_SOURCE) {readingPositionUs = startPositionUs;}streamFormats = formats;streamOffsetUs = offsetUs;onStreamChanged(formats, startPositionUs, offsetUs);//子类实现}@Overridepublic final void start() throws ExoPlaybackException {Assertions.checkState(state == STATE_ENABLED);state = STATE_STARTED;//改变状态onStarted();//子类实现}//BaseRenderer还提供了readSource方法,用于读取Sample中的数据//readFlags知道当前需要获取的数据类型protected final @ReadDataResult int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {@ReadDataResult//这里的stream最终从上文讲的SampleQueue中获取数据int result = Assertions.checkNotNull(stream).readData(formatHolder, buffer, readFlags);if (result == C.RESULT_BUFFER_READ) {//当前获取的是BUFFER数据if (buffer.isEndOfStream()) {readingPositionUs = C.TIME_END_OF_SOURCE;return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;}buffer.timeUs += streamOffsetUs;readingPositionUs = max(readingPositionUs, buffer.timeUs);} else if (result == C.RESULT_FORMAT_READ) {//当前获取的是Format数据Format format = Assertions.checkNotNull(formatHolder.format);if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {format =format.buildUpon().setSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs).build();formatHolder.format = format;}}return result;}
BaseRenderer的实现比较简单,重点看下它的子类,这里主要学习下MediaCodecRenderer,在看MediaCodecRenderer前得先了解下Android系统的MediaCodec。
MediaCodec
MediaCodec 可用于访问低级媒体编解码器,即编码器/解码器组件。它是 Android 低级多媒体支持基础设施的一部分,这里主要了解下解码过程,不知道还有没有读者记得这张在讲SampleQueue里出现的图了
解码器的作用就是处理输入的编码数据输出解码后的数据。它使用一组输入和输出缓冲区异步处理数据。首先,调用者初始化MediaCodec.configure,向MediaCodec.dequeueInputBuffer请求一个空的输入缓冲区,将其他地方读取到的编码数据填充输入缓冲区,queueInputBuffer将其发送给MediaCodec进行处理。MediaCodec获取的输入缓冲数据进行解码,完成后将解码后的数据输出至输出缓冲区。最后,调用者向MediaCodec.dequeueOutputBuffer请求已填充的输出缓冲区,调用者将获取输出缓冲区的解码数据将其渲染到指定地方,使用完成后releaseOutputBuffer将其释放回MediaCodec。如果在MediaCodec.configure传入了Surface,releaseOutputBuffer后会将解码数据直接渲染到传入的Surface上。
在MediaCodec生命周期中,存在以下三种状态:Stopped、Executing 、 Released。 Stopped 状态实际上是三个状态的组合:Uninitialized、Configured 和 Error,而 Executing 状态会经历三个子状态:Flushed、Running 和 End-of-Stream。
当创建MediaCodec时,编解码器处于Uninitialized状态。首先,您需要通过configure对其进行配置,这会将其置于Configured 状态,然后调用start将其置于Executing 状态。在Executing 状态下,就可以通过上述缓冲区队列操作来处理数据了。
Executing 状态具有三个子状态:Flushed、Running 和 End-of-Stream。在 start之后,MediaCodec立即处于 Flushed 子状态,其中保存所有缓冲区。一旦第一个输入缓冲区出队,编解码器就会进入Running 子状态,大部分时间会执行在此状态下。当使用BUFFER_FLAG_END_OF_STREAM Flag标记进行MediaCodec.queueInputBuffer时,MediaCodec将转换到End-of-Stream子状态。在此状态下,编解码器不再接受更多输入缓冲区,但仍生成输出缓冲区,直到输出到达流末尾。对于解码器,可以在处于 Executing 状态时随时使用 flash返回到 Flushed 子状态。
调用 stop 将编解码器返回到Uninitialized状态,然后可以再次configure它。使用完编解码器后,必须通过调用release来释放它。
有了上面的知识,来看看看MediaCodecRenderer是如何使用这些方法,完成整个渲染的。
MediaCodecRenderer
MediaCodecRenderer主要是通过Android的MediaCodec来渲染解码渲染出音视频内容,主要有2个子类MediaCodecVideoRenderer和MediaCodecAudioRenderer。直接看下render的实现
@Override//positionUs为当前的播放时间戳,如果有音轨会获取音轨的PTS//elapsedRealtimeUs循环调用render开始前的时间戳,组要用来计算程序的执行时长public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {...// We have a format.maybeInitCodecOrBypass();//如果是直出的也就是不需要Codec解码的数据if (bypassEnabled) {TraceUtil.beginSection("bypassRender");//while (bypassRender(positionUs, elapsedRealtimeUs)) {}TraceUtil.endSection();} else if (codec != null) {//需要通过MeidaCodec解码的数据//记录循环开始时间,用于计算执行时间是否超过renderTimeLimitMs,决定是否继续循环long renderStartTimeMs = SystemClock.elapsedRealtime();TraceUtil.beginSection("drainAndFeed");//先从MediaCodec中获取已解码数据while (drainOutputBuffer(positionUs, elapsedRealtimeUs)&& shouldContinueRendering(renderStartTimeMs)) {}//向MediaCodec输入待解码数据while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {}TraceUtil.endSection();}...}protected final void maybeInitCodecOrBypass() throws ExoPlaybackException {...if (isBypassPossible(inputFormat)) {initBypass(inputFormat);//对于不需要Codec解码的数据直接,初始化Bypass主要就是初始化Byapass的buffer:bypassBatchBufferreturn;}...//初始化MediaCodecmaybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);...}private void maybeInitCodecWithFallback(@Nullable MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)throws DecoderInitializationException {if (availableCodecInfos == null) {try {//通过输入的数据的Meta信息获取用于初始化Codec的相关数据List<MediaCodecInfo> allAvailableCodecInfos =getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);...}...//开始初始化CodecinitCodec(codecInfo, crypto);
...}private void initCodec(MediaCodecInfo codecInfo, @Nullable MediaCrypto crypto) throws Exception {//通过子类获取MediaCodecAdapter.ConfigurationMediaCodecAdapter.Configuration configuration =getMediaCodecConfiguration(codecInfo, inputFormat, crypto, codecOperatingRate);...//创建出MediaCodecAdapter,这里的Adapter主要有2个实现,一个是SynchronousMediaCodecAdapter 通过同步的方式调用MediaCodec,一个是针对API23的异步MeidaCodec调用的AsynchronousMediaCodecAdaptertry {TraceUtil.beginSection("createCodec:" + codecName);codec = codecAdapterFactory.createAdapter(configuration);} finally {TraceUtil.endSection();}...}//这里为了方便看下SynchronousMediaCodecAdapter public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException {@Nullable MediaCodec codec = null;try {//主要通过MediaCodec.createByCodecName(codecName)创建出MediaCodeccodec = createCodec(configuration);TraceUtil.beginSection("configureCodec");//配置MediaCodeccodec.configure(//格式,其中KEY_MAX_INPUT_SIZE确定了缓冲区的大小,对应于format.maxInputSize,可以查看计算逻辑configuration.mediaFormat,configuration.surface,//渲染的surfaceconfiguration.crypto,configuration.flags);TraceUtil.endSection();TraceUtil.beginSection("startCodec");//到这里MediaCodec就已经准备好了随时可以用来解码了codec.start();TraceUtil.endSection();return new SynchronousMediaCodecAdapter(codec);} catch (IOException | RuntimeException e) {if (codec != null) {codec.release();}throw e;}}//直出数据渲染private boolean bypassRender(long positionUs, long elapsedRealtimeUs)throws ExoPlaybackException {...//有数据后调用子类processOutputBuffer渲染数据if (bypassBatchBuffer.hasSamples()) {if (processOutputBuffer(//这里调用MediaCodecAudioRendererpositionUs,elapsedRealtimeUs,/* codec= */ null,bypassBatchBuffer.data,outputIndex,/* bufferFlags= */ 0,bypassBatchBuffer.getSampleCount(),bypassBatchBuffer.getFirstSampleTimeUs(),bypassBatchBuffer.isDecodeOnly(),bypassBatchBuffer.isEndOfStream(),outputFormat)) {// The batch buffer has been fully processed.onProcessedOutputBuffer(bypassBatchBuffer.getLastSampleTimeUs());bypassBatchBuffer.clear();} else {// Could not process the whole batch buffer. Try again later.return false;}}...// 第一次先读取Sample数据到bypassBatchBufferbypassRead();
...}private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)throws ExoPlaybackException {//如果成功渲染了OutputBuffer,OutputBuffer会重置,这里会hasOutputBuffer=false会拉取下一段Buffer继续执行//如果当前的Buffer因某种原因,如渲染过快需要等待,这个时候OutputBuffer还是上次未渲染的数据if (!hasOutputBuffer()) {int outputIndex;...//获取解码后的OutputBuffer的索引outputIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);}if (outputIndex < 0) {//异常情况//格式变更if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) {processOutputMediaFormatChanged();return true;}// MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value.if (codecNeedsEosPropagation&& (inputStreamEnded || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM)) {processEndOfStream();}return false;}// 跳过特殊机型的适配数据if (shouldSkipAdaptationWorkaroundOutputBuffer) {shouldSkipAdaptationWorkaroundOutputBuffer = false;codec.releaseOutputBuffer(outputIndex, false);return true;} else if (outputBufferInfo.size == 0&& (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {// dequeued buffer 标记了结束Flag,立即结束processEndOfStream();return false;}this.outputIndex = outputIndex;//更新全局的outputIndex//通过outputIndex 获取OutputBufferoutputBuffer = codec.getOutputBuffer(outputIndex);//根据outputBufferInfo初始化outputBufferif (outputBuffer != null) {outputBuffer.position(outputBufferInfo.offset);outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);}...}boolean processedOutputBuffer;if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {processedOutputBuffer =processOutputBuffer(//调用子类处理OutputBufferpositionUs,elapsedRealtimeUs,codec,outputBuffer,outputIndex,outputBufferInfo.flags,/* sampleCount= */ 1,outputBufferInfo.presentationTimeUs,isDecodeOnlyOutputBuffer,isLastOutputBuffer,outputFormat);}//成功渲染了当前OutputBufferif (processedOutputBuffer) {onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;resetOutputBuffer();//重置OutputBufferif (!isEndOfStream) {return true;}processEndOfStream();}//否则返回false中止drainOutputBuffer,进入feedInputBufferreturn false;}//向MediaCodec输入数据private boolean feedInputBuffer() throws ExoPlaybackException {...if (inputIndex < 0) {//如果已经resetInputBuffer//获取InputBuffer索引inputIndex = codec.dequeueInputBufferIndex();//这里可能获取不到,有可能MediaCodec的缓存已经满了,此时就不再读取数据输入了//这个MediaCodec最大的缓存大小是在MediaCodec初始化时传入Format时确定的if (inputIndex < 0) {return false;}//通过索引获取InputBufferbuffer.data = codec.getInputBuffer(inputIndex);//清空数据buffer.clear();}//需要消耗当前InputBufferif (codecDrainState == DRAIN_STATE_SIGNAL_END_OF_STREAM) {// We need to re-initialize the codec. Send an end of stream signal to the existing codec so// that it outputs any remaining buffers before we release it.if (codecNeedsEosPropagation) {// Do nothing.} else {codecReceivedEos = true;codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);resetInputBuffer();}codecDrainState = DRAIN_STATE_WAIT_END_OF_STREAM;return false;}//特殊适配,在InputBuffer前加入三个H.264 NAL单元:SPS、PPS和 32 * 32 像素 IDR slice,可以强制Format更新if (codecNeedsAdaptationWorkaroundBuffer) {codecNeedsAdaptationWorkaroundBuffer = false;buffer.data.put(ADAPTATION_WORKAROUND_BUFFER);codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0);resetInputBuffer();codecReceivedBuffers = true;return true;}//对于自适应重配置,解码器期望在缓冲区的开头提供重配置数据if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {for (int i = 0; i < codecInputFormat.initializationData.size(); i++) {byte[] data = codecInputFormat.initializationData.get(i);buffer.data.put(data);}codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;}int adaptiveReconfigurationBytes = buffer.data.position();FormatHolder formatHolder = getFormatHolder();@SampleStream.ReadDataResult int result;try {//开始读取数据到InputBuffer里result = readSource(formatHolder, buffer, /* readFlags= */ 0);} catch (InsufficientCapacityException e) {onCodecError(e);//对于过大的Sample,直接读取Mate信息跳过数据Sample读取readSourceOmittingSampleData(/* readFlags= */ 0);flushCodec();return true;}
...if (result == C.RESULT_FORMAT_READ) {//读取的是Mate信息if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {// We received two formats in a row. Clear the current buffer of any reconfiguration data// associated with the first format.buffer.clear();codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;}onInputFormatChanged(formatHolder);return true;}...long presentationTimeUs = buffer.timeUs;//获取PTS...largestQueuedPresentationTimeUs = max(largestQueuedPresentationTimeUs, presentationTimeUs);buffer.flip();//切换为readif (buffer.hasSupplementalData()) {handleInputBufferSupplementalData(buffer);}onQueueInputBuffer(buffer);try {//将包含未解码数据的InputBuffer传给MediaCodec解码if (bufferEncrypted) {codec.queueSecureInputBuffer(inputIndex, /* offset= */ 0, buffer.cryptoInfo, presentationTimeUs, /* flags= */ 0);} else {codec.queueInputBuffer(inputIndex, /* offset= */ 0, buffer.data.limit(), presentationTimeUs, /* flags= */ 0);}} catch (CryptoException e) {throw createRendererException(e, inputFormat, Util.getErrorCodeForMediaDrmErrorCode(e.getErrorCode()));}resetInputBuffer();//重置InputBuffer,下次会读取新的InputBuffercodecReceivedBuffers = true;codecReconfigurationState = RECONFIGURATION_STATE_NONE;decoderCounters.queuedInputBufferCount++;return true;}
processOutputBuffer交由子类实现也就是MediaCodecVideoRenderer和MediaCodecAudioRenderer,MediaCodecAudioRenderer实现相对MediaCodecVideoRenderer简单
MediaCodecAudioRenderer
音频渲染器,主要通过Android系统的AudioTrack实现音频播放
@Overrideprotected boolean processOutputBuffer(long positionUs,long elapsedRealtimeUs,@Nullable MediaCodecAdapter codec,@Nullable ByteBuffer buffer,int bufferIndex,int bufferFlags,int sampleCount,long bufferPresentationTimeUs,boolean isDecodeOnlyBuffer,boolean isLastBuffer,Format format)throws ExoPlaybackException {checkNotNull(buffer);if (decryptOnlyCodecFormat != null//包含编解码器初始化/编解码器特定数据而不是媒体数据,直接release掉&& (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// Discard output buffers from the passthrough (raw) decoder containing codec specific data.checkNotNull(codec).releaseOutputBuffer(bufferIndex, false);return true;}if (isDecodeOnlyBuffer) {//无需渲染的数据直接release掉if (codec != null) {codec.releaseOutputBuffer(bufferIndex, false);}decoderCounters.skippedOutputBufferCount += sampleCount;audioSink.handleDiscontinuity();return true;}boolean fullyConsumed;try {//开始渲染出数据,调用DefaultAudioSink播放这些数据fullyConsumed = audioSink.handleBuffer(buffer, bufferPresentationTimeUs, sampleCount);} catch (InitializationException e) {throw createRendererException(e, inputFormat, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED);} catch (WriteException e) {throw createRendererException(e, format, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED);}if (fullyConsumed) {//渲染完毕,releaseif (codec != null) {codec.releaseOutputBuffer(bufferIndex, false);}decoderCounters.renderedOutputBufferCount += sampleCount;return true;}return false;}//DefaultAudioSink@Override@SuppressWarnings("ReferenceEquality")public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount)throws InitializationException, WriteException {...//初始化创建出AudioTrack对象if (!isAudioTrackInitialized()) {try {if (!initializeAudioTrack()) {// Not yet ready for initialization of a new AudioTrack.return false;}} catch (InitializationException e) {if (e.isRecoverable) {throw e; // Do not delay the exception if it can be recovered at higher level.}initializationExceptionPendingExceptionHolder.throwExceptionIfDeadlineIsReached(e);return false;}}initializationExceptionPendingExceptionHolder.clear();if (startMediaTimeUsNeedsInit) {//首次执行startMediaTimeUs = max(0, presentationTimeUs);startMediaTimeUsNeedsSync = false;startMediaTimeUsNeedsInit = false;if (useAudioTrackPlaybackParams()) {setAudioTrackPlaybackParametersV23();}applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);//audioTrack.play()if (playing) {play();}}
...// 校验 presentationTimeUslong expectedPresentationTimeUs =startMediaTimeUs//播放开始时间+ configuration.inputFramesToDurationUs(//帧数除以音频采样率计算出到这一帧的标准时长getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount());if (!startMediaTimeUsNeedsSync//和计算的标准时间相差了200ms&& Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) {if (listener != null) {listener.onAudioSinkError(new AudioSink.UnexpectedDiscontinuityException(presentationTimeUs, expectedPresentationTimeUs));}startMediaTimeUsNeedsSync = true;//标记开始时间需要同步}if (startMediaTimeUsNeedsSync) {//同步startMediaTimeUsif (!drainToEndOfStream()) {// Don't update timing until pending AudioProcessor buffers are completely drained.return false;}// 开始调整startMediaTimeUs//获取时间差long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs;//重新设置startMediaTimeUsstartMediaTimeUs += adjustmentUs;startMediaTimeUsNeedsSync = false;// Re-apply playback parameters because the startMediaTimeUs changed.applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);if (listener != null && adjustmentUs != 0) {listener.onPositionDiscontinuity();}}//总提交帧数增加if (configuration.outputMode == OUTPUT_MODE_PCM) {submittedPcmBytes += buffer.remaining();} else {submittedEncodedFrames += (long) framesPerEncodedSample * encodedAccessUnitCount;}inputBuffer = buffer;inputBufferAccessUnitCount = encodedAccessUnitCount;}//最终调用audioTrack.write写入数据,完成音频输出processBuffers(presentationTimeUs);if (!inputBuffer.hasRemaining()) {inputBuffer = null;inputBufferAccessUnitCount = 0;return true;}if (audioTrackPositionTracker.isStalled(getWrittenFrames())) {Log.w(TAG, "Resetting stalled audio track");flush();return true;}return false;}
可以看到MediaCodecAudioRenderer直接使用了输入的bufferPresentationTimeUs作为PTS将音频输出,期间没有进行过调整,只是调整了startMediaTimeUs 开始时间,所以实现简单几乎不涉及任何的时间同步代码。这里可以确定Exoplayer可能采用音频的PTS作为参考时钟,在播放视频时,以音频时钟为准将视频时间同步到音频上。下面就证实下上面的猜想,看下MediaCodecVideoRenderer的实现。
MediaCodecVideoRenderer
视频数据在调用MediaCodec.releaseOutputBuffer后就会渲染到指定的Surface上,这个过程就主要执行在MediaCodecVideoRenderer里
@Overrideprotected boolean processOutputBuffer(long positionUs,//参考时钟的播放位置,对应Audio的PTSlong elapsedRealtimeUs,//循环调用render开始前的时间戳,主要用来计算程序的执行时长@Nullable MediaCodecAdapter codec,@Nullable ByteBuffer buffer,int bufferIndex,int bufferFlags,int sampleCount,long bufferPresentationTimeUs,//视频流的PTSboolean isDecodeOnlyBuffer,boolean isLastBuffer,Format format)throws ExoPlaybackException {checkNotNull(codec); // 视频必须要codec解码if (initialPositionUs == C.TIME_UNSET) {initialPositionUs = positionUs;//第一次初始化位置赋值}//更新上一次的bufferPresentationTimeUsif (bufferPresentationTimeUs != lastBufferPresentationTimeUs) {if (!videoFrameProcessorManager.isEnabled()) {frameReleaseHelper.onNextFrame(bufferPresentationTimeUs);} // else, update the frameReleaseHelper when releasing the processed frames.this.lastBufferPresentationTimeUs = bufferPresentationTimeUs;}//获取流开始PTSlong outputStreamOffsetUs = getOutputStreamOffsetUs();//当前的PTS-开始PTS=PTS时长long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;if (isDecodeOnlyBuffer && !isLastBuffer) {skipOutputBuffer(codec, bufferIndex, presentationTimeUs);return true;}// Note: Use of double rather than float is intentional for accuracy in the calculations below.boolean isStarted = getState() == STATE_STARTED;//获取当前系统时间long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;long earlyUs =//提前时长=使用当前流的PTS-参考时钟-程序执行时长calculateEarlyTimeUs(positionUs,elapsedRealtimeUs,elapsedRealtimeNowUs,bufferPresentationTimeUs,isStarted);if (displaySurface == placeholderSurface) {// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.if (isBufferLate(earlyUs)) {skipOutputBuffer(codec, bufferIndex, presentationTimeUs);updateVideoFrameProcessingOffsetCounters(earlyUs);return true;}return false;}//当前帧已经延迟超过30ms(earlyUs<-30000),且距离上次渲染时间已经超过了100ms,此时画面是静止的,需要强制去渲染当前帧boolean forceRenderOutputBuffer = shouldForceRender(positionUs, earlyUs);if (forceRenderOutputBuffer) {//强制渲染场景boolean notifyFrameMetaDataListener;if (videoFrameProcessorManager.isEnabled()) {notifyFrameMetaDataListener = false;if (!videoFrameProcessorManager.maybeRegisterFrame(format, presentationTimeUs, isLastBuffer)) {return false;}} else {notifyFrameMetaDataListener = true;}renderOutputBufferNow(//开始渲染codec, format, bufferIndex, presentationTimeUs, notifyFrameMetaDataListener);updateVideoFrameProcessingOffsetCounters(earlyUs);return true;}if (!isStarted || positionUs == initialPositionUs) {return false;}// 计算提交给Codec也就是releaseOutputBuffer时,指定的送显时间戳long systemTimeNs = System.nanoTime();//当前时间+提前的时长long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);// 进一步调整精确送显时间戳,后面会看到具体代码long adjustedReleaseTimeNs = frameReleaseHelper.adjustReleaseTime(unadjustedFrameReleaseTimeNs);if (!videoFrameProcessorManager.isEnabled()) {//使用精确的送显时间重新计算earlyUsearlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;}//丢帧逻辑boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET;if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer)&& maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped)) {return false;} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) {if (treatDroppedBuffersAsSkipped) {skipOutputBuffer(codec, bufferIndex, presentationTimeUs);} else {dropOutputBuffer(codec, bufferIndex, presentationTimeUs);}updateVideoFrameProcessingOffsetCounters(earlyUs);return true;}...if (Util.SDK_INT >= 21) {// 大于等于21,这里直接传入送显时间,让Codec决定什么时候送显if (earlyUs < 50000) {//舍弃提前太多的帧,最多渲染提前50ms送显的帧if (adjustedReleaseTimeNs == lastFrameReleaseTimeNs) {//2次送显时间一致,说明渲染速率要比显示器刷新率快,尽快跳过当前帧,保证渲染速率skipOutputBuffer(codec, bufferIndex, presentationTimeUs);} else {//触发送显的监听notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);//使用adjustedReleaseTimeNs送显时间releaseOutputBufferrenderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);}updateVideoFrameProcessingOffsetCounters(earlyUs);lastFrameReleaseTimeNs = adjustedReleaseTimeNs;return true;}} else {// 21以下系统需要自己控制送显时间,releaseOutputBuffer不支持传入送显时间if (earlyUs < 30000) {//舍弃提前太多的帧,最多渲染提前30ms送显的帧,至于为啥是30ms,问就是感觉if (earlyUs > 11000) {//如果在11m到30ms之间,还是有点早,需要阻塞等待// Note: The 11ms threshold was chosen fairly arbitrarily.//11ms没有太多依据,凭感觉try {// 保证至少1msThread.sleep((earlyUs - 10000) / 1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}//触发送显的监听notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);//低于11m的就直接送显了renderOutputBuffer(codec, bufferIndex, presentationTimeUs);//直接送显updateVideoFrameProcessingOffsetCounters(earlyUs);return true;}}// 返回false可能当前还未播放或者还没到渲染这帧的时间return false;}//计算提前时长private long calculateEarlyTimeUs(long positionUs,long elapsedRealtimeUs,long elapsedRealtimeNowUs,long bufferPresentationTimeUs,boolean isStarted) {// Note: Use of double rather than float is intentional for accuracy in the calculations below.double playbackSpeed = getPlaybackSpeed();//计算比当前播放的时间提前了多久,换句话说就是当前帧在真实需要渲染时间前提前了多久开始渲染。负值说明已经画面延迟了,我们在需要的时间并没有提供相应的渲染数据。long earlyUs = (long) ((bufferPresentationTimeUs - positionUs) / playbackSpeed);if (isStarted) {// 这里计算减去程序执行到这里所用的耗时earlyUs -= elapsedRealtimeNowUs - elapsedRealtimeUs;}return earlyUs;}@RequiresApi(21)protected void renderOutputBufferV21(MediaCodecAdapter codec, int index, long presentationTimeUs, long releaseTimeNs) {TraceUtil.beginSection("releaseOutputBuffer");codec.releaseOutputBuffer(index, releaseTimeNs);//这里传入了送显时间,由底层控制送显时间TraceUtil.endSection();decoderCounters.renderedOutputBufferCount++;consecutiveDroppedFrameCount = 0;if (!videoFrameProcessorManager.isEnabled()) {lastRenderRealtimeUs = SystemClock.elapsedRealtime() * 1000;maybeNotifyVideoSizeChanged(decodedVideoSize);maybeNotifyRenderedFirstFrame();}}protected void renderOutputBuffer(MediaCodecAdapter codec, int index, long presentationTimeUs) {TraceUtil.beginSection("releaseOutputBuffer");codec.releaseOutputBuffer(index, true);//低于21的系统,这里直接就送显了,true表示会渲染到Codec指定的Surface上TraceUtil.endSection();decoderCounters.renderedOutputBufferCount++;consecutiveDroppedFrameCount = 0;if (!videoFrameProcessorManager.isEnabled()) {lastRenderRealtimeUs = SystemClock.elapsedRealtime() * 1000;maybeNotifyVideoSizeChanged(decodedVideoSize);maybeNotifyRenderedFirstFrame();}}
//看下VideoFrameReleaseHelper的进一步调整精确送显时间戳的过程public long adjustReleaseTime(long releaseTimeNs) {// Until we know better, the adjustment will be a no-op.long adjustedReleaseTimeNs = releaseTimeNs;//同步状态下执行,所谓Synced指获取到连续的15个帧间隔时间小于1ms的帧if (lastAdjustedFrameIndex != C.INDEX_UNSET && frameRateEstimator.isSynced()) {//用这些帧的总时常/帧数=平局的帧间隔时长long frameDurationNs = frameRateEstimator.getFrameDurationNs();long candidateAdjustedReleaseTimeNs =lastAdjustedReleaseTimeNs//预测当前帧送显时间=上次帧的送显时间+当前帧到上次帧的帧数*帧间间隔时长/播放速度+ (long) ((frameDurationNs * (frameIndex - lastAdjustedFrameIndex)) / playbackSpeed);//如果当前送显时间和预测的送显时间相隔时长小等于20ms,则使用预测的送显时间//这里20ms主要是考虑Android VSYNC机制,送显的数据不是立即显示到屏幕上,而是经过3级的缓存,在接收到VSYNC信号时才会显示到屏幕上,也就是你期望的送显时间并不是实际的送显时间if (adjustmentAllowed(releaseTimeNs, candidateAdjustedReleaseTimeNs)) {adjustedReleaseTimeNs = candidateAdjustedReleaseTimeNs;} else {resetAdjustment();}}pendingLastAdjustedFrameIndex = frameIndex;pendingLastAdjustedReleaseTimeNs = adjustedReleaseTimeNs;//下面是基于Vsync信号时间戳来调整送显时间戳,保证帧数据尽快显示到屏幕上if (vsyncSampler == null || vsyncDurationNs == C.TIME_UNSET) {return adjustedReleaseTimeNs;}//获取当前的Vsync信号时间戳long sampledVsyncTimeNs = vsyncSampler.sampledVsyncTimeNs;if (sampledVsyncTimeNs == C.TIME_UNSET) {return adjustedReleaseTimeNs;}// 寻找距离当前送显时间戳最近的目标Vsync信号时间戳long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs);// 减去一个vsyncOffsetNs,保证送显时间在前一个Vsync信号时间戳前,目标Vsync信号时间戳之后//这个vsyncOffsetNs计算方式://1.获取当前屏幕的刷新率,如60Hz就是屏幕每秒刷新60帧//2.计算每帧间隔时长,60Hz每帧间隔就是1/60秒,也就就是16.6ms//3.用这个间隔X0.8,vsyncOffsetNs=16.6ms*0.8=13.28msreturn snappedTimeNs - vsyncOffsetNs;}
可以看到MediaCodecVideoRenderer参考positionUs时间,和当前流的PTS进行时间同步,保证同步。貌似目前还看不出和MediaCodecAudioRenderer 音频PTS的关系,但可以肯定视频的PTS是参考其他时间进行同步的,为了达到同步ExoPlayer用了大量的代码,还考虑了程序的执行时间,以纳秒级的计算,尽量缩小了误差,在极端情况下还会直接通过丢帧的方式保证同步(这也就是有时候播放的文件解码压力比较大时,视频会一卡一卡但是音频还是流畅播放的原因,可以思考下为什么这么做,反过来行不行)。
参考时间戳的计算
MediaCodecVideoRenderer的参考positionUs在有音轨的情况下,是通过MediaCodecAudioRenderer获取的,MediaCodecAudioRenderer又通过调用DefaultAudiaSkink,最终调用AudiaTrack的方法获取时间戳,下面我们来看下具体获取的过程
private void updateCurrentPosition() {long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {currentPositionUs =allowPositionDiscontinuity? newCurrentPositionUs: max(currentPositionUs, newCurrentPositionUs);allowPositionDiscontinuity = false;}}//DefaultAudioSink@Overridepublic long getCurrentPositionUs(boolean sourceEnded) {if (!isAudioTrackInitialized() || startMediaTimeUsNeedsInit) {return CURRENT_POSITION_NOT_SET;}//主要从这里获取long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded);//和通过帧数获取的时长位置取最小值positionUs = min(positionUs, configuration.framesToDurationUs(getWrittenFrames()));return applySkipping(applyMediaPositionParameters(positionUs));}public long getCurrentPositionUs(boolean sourceEnded) {if (checkNotNull(this.audioTrack).getPlayState() == PLAYSTATE_PLAYING) {//如果已经开始播放从AudiaTrack中同步出下面逻辑需要使用的数据,以及获取smoothedPlayheadOffsetUs,对getPlaybackHeadPositionUs做一个平滑处理maybeSampleSyncParams();}long systemTimeUs = System.nanoTime() / 1000;long positionUs;AudioTimestampPoller audioTimestampPoller = checkNotNull(this.audioTimestampPoller);boolean useGetTimestampMode = audioTimestampPoller.hasAdvancingTimestamp();if (useGetTimestampMode) {//如果支持AudioTrack.getTimestamp优先使用// Calculate the speed-adjusted position using the timestamp (which may be in the future).long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();//获取当前帧数long timestampPositionUs = framesToDurationUs(timestampPositionFrames);//帧数转为时长long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs();elapsedSinceTimestampUs =//计算和当前时间的差值Util.getMediaDurationForPlayoutDuration(elapsedSinceTimestampUs, audioTrackPlaybackSpeed);positionUs = timestampPositionUs + elapsedSinceTimestampUs;//当前的帧时长+当前时间的差值=当前位置} else {//否则使用getPlaybackHeadPositionUs的值if (playheadOffsetCount == 0) {// 刚开始播放,没有足够多的数据计算平滑差值,直接取getPlaybackHeadPositionUspositionUs = getPlaybackHeadPositionUs();} else {// getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off// the system clock (and a smoothed offset between it and the playhead position) so as to// prevent jitter in the reported positions.//AudiaTrack.getPlaybackHeadPositionUs获取的精度只有20ms,所以需要和当前时间的差值求一个平滑差值,防止获取的getPlaybackHeadPositionUs有抖动positionUs =Util.getMediaDurationForPlayoutDuration(systemTimeUs + smoothedPlayheadOffsetUs, audioTrackPlaybackSpeed);}if (!sourceEnded) {//最终的位置还需要减去一个底层的延迟positionUs = max(0, positionUs - latencyUs);}}if (lastSampleUsedGetTimestampMode != useGetTimestampMode) {// 2次获取当前位置的方式不一样,保存上一次的值previousModeSystemTimeUs = lastSystemTimeUs;previousModePositionUs = lastPositionUs;}long elapsedSincePreviousModeUs = systemTimeUs - previousModeSystemTimeUs;//模式切换且和当前时间相差1s以内,在1s内对上次的位置到当前时间做一个平滑过渡,防止模式切换导致的跳动if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) {long previousModeProjectedPositionUs =previousModePositionUs+ Util.getMediaDurationForPlayoutDuration(elapsedSincePreviousModeUs, audioTrackPlaybackSpeed);// 1s内取样1000次平滑过渡到当前时间long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US;positionUs *= rampPoint;positionUs += (1000 - rampPoint) * previousModeProjectedPositionUs;positionUs /= 1000;}//需要监听位置首次增加的场景if (!notifiedPositionIncreasing && positionUs > lastPositionUs) {notifiedPositionIncreasing = true;long mediaDurationSinceLastPositionUs = Util.usToMs(positionUs - lastPositionUs);long playoutDurationSinceLastPositionUs =Util.getPlayoutDurationForMediaDuration(mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed);long playoutStartSystemTimeMs =//获取开始时间System.currentTimeMillis() - Util.usToMs(playoutDurationSinceLastPositionUs);listener.onPositionAdvancing(playoutStartSystemTimeMs);}lastSystemTimeUs = systemTimeUs;lastPositionUs = positionUs;lastSampleUsedGetTimestampMode = useGetTimestampMode;return positionUs;}private void maybeSampleSyncParams() {//获取当前时间long systemTimeUs = System.nanoTime() / 1000;//保证间隔30ms调用if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {//通过AudiaTrack.getPlaybackHeadPosition获取当前播放位置long playbackPositionUs = getPlaybackHeadPositionUs();if (playbackPositionUs == 0) {// 音频可能还未播放return;}// 最多取前10次playbackPositionUs 和当前时间的差值,求出平均差值,对playbackPositionUs 做一个平滑处理playheadOffsets[nextPlayheadOffsetIndex] =//获取10次的差值存储Util.getPlayoutDurationForMediaDuration(playbackPositionUs, audioTrackPlaybackSpeed)- systemTimeUs;//每10次一个循环nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {playheadOffsetCount++;}lastPlayheadSampleTimeUs = systemTimeUs;smoothedPlayheadOffsetUs = 0;//获取前几次差值的平均值,获得平滑的差值,后续通过当前时间+这个值就可以计算出当前的PlaybackHeadPositionfor (int i = 0; i < playheadOffsetCount; i++) {smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;}}if (needsPassthroughWorkarounds) {//对于API 21/22的AC-3直出音轨,后续获取的timestamp和latency 都是错误的值,这里直接跳过return;}//audioTrack.getTimestamp获取timestampmaybePollAndCheckTimestamp(systemTimeUs);//audioTrack.getLatency获取底层的延迟maybeUpdateLatency(systemTimeUs);}private long getPlaybackHeadPositionUs() {return framesToDurationUs(getPlaybackHeadPosition());}private long getPlaybackHeadPosition() {//获取当前时间long currentTimeMs = SystemClock.elapsedRealtime();if (stopTimestampUs != C.TIME_UNSET) {//已经停止// Simulate the playback head position up to the total number of frames submitted.//获取当前到结束位置的时长long elapsedTimeSinceStopUs = (currentTimeMs * 1000) - stopTimestampUs;//根据播放速度纠正时长long mediaTimeSinceStopUs =Util.getMediaDurationForPlayoutDuration(elapsedTimeSinceStopUs, audioTrackPlaybackSpeed);//时长转帧数long framesSinceStop = durationUsToFrames(mediaTimeSinceStopUs);//结束位置获取的总帧数+结束位置到现在的帧数=现在的总帧数,再和结束位置以写入的总帧数取最小值return min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);}//正常情况走下面逻辑,保证间隔5ms调用一次if (currentTimeMs - lastRawPlaybackHeadPositionSampleTimeMs>= RAW_PLAYBACK_HEAD_POSITION_UPDATE_INTERVAL_MS) {updateRawPlaybackHeadPosition(currentTimeMs);lastRawPlaybackHeadPositionSampleTimeMs = currentTimeMs;}return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);}private void updateRawPlaybackHeadPosition(long currentTimeMs) {AudioTrack audioTrack = checkNotNull(this.audioTrack);int state = audioTrack.getPlayState();if (state == PLAYSTATE_STOPPED) {// The audio track hasn't been started. Keep initial zero timestamp.return;}//最终调用audioTrack.getPlaybackHeadPosition获取时长,获取的为底层的无符号整型,java中通过有符号的long来表示long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();if (needsPassthroughWorkarounds) {//这块是一个兼容处理,对于API 21/22的直出音轨,在暂停时获取到的值可能为0if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {//保存为0时的位置passthroughWorkaroundPauseOffset = this.rawPlaybackHeadPosition;}//这里进行恢复rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;}if (Util.SDK_INT <= 29) {if (rawPlaybackHeadPosition == 0&& this.rawPlaybackHeadPosition > 0&& state == PLAYSTATE_PLAYING) {//这段也是一个兼容处理,当API<=29使用蓝牙设备播放时,连接蓝牙失败时,底层的状态已经停止,但JAVA层的状态还是正在播放//当这种情况发生时获取位置为0if (forceResetWorkaroundTimeMs == C.TIME_UNSET) {//通过设置这个标记来告诉当前处于错误状态,当超过200ms后还是有问题,会尝试重新初始化forceResetWorkaroundTimeMs = currentTimeMs;}return;} else {forceResetWorkaroundTimeMs = C.TIME_UNSET;}}if (this.rawPlaybackHeadPosition > rawPlaybackHeadPosition) {// The value must have wrapped around.rawPlaybackHeadWrapCount++;}this.rawPlaybackHeadPosition = rawPlaybackHeadPosition;}//audioTrack.getTimestamp获取timestampprivate void maybePollAndCheckTimestamp(long systemTimeUs) {//audioTrack.getTimestamp不能平凡调用,AudioTimestampPoller 是一个Audio Timestamp的轮询获取器,稳定后控制调用者以10s的间隔去获取TimestampAudioTimestampPoller audioTimestampPoller = checkNotNull(this.audioTimestampPoller);//是否获取到新的Timestamp,条件是API必须大于等于19以支持这个函数,且符合指定的时间间隔if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) {return;}// 检验获取的Timestamplong audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs();long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();long playbackPositionUs = getPlaybackHeadPositionUs();//不能和系统时间相差太大,>5sif (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {listener.onSystemTimeUsMismatch(audioTimestampPositionFrames,audioTimestampSystemTimeUs,systemTimeUs,playbackPositionUs);audioTimestampPoller.rejectTimestamp();//不能和getPlaybackHeadPositionUs方法获取的值相差太大,>5s} else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs)> MAX_AUDIO_TIMESTAMP_OFFSET_US) {listener.onPositionFramesMismatch(audioTimestampPositionFrames,audioTimestampSystemTimeUs,systemTimeUs,playbackPositionUs);audioTimestampPoller.rejectTimestamp();} else {audioTimestampPoller.acceptTimestamp();}}//audioTrack.getTimestamp.getLatency获取底层的延迟private void maybeUpdateLatency(long systemTimeUs) {if (isOutputPcm//线性 PCM 编码&& getLatencyMethod != null//AudiaTreck存在getLatency方法&& systemTimeUs - lastLatencySampleTimeUs >= MIN_LATENCY_SAMPLE_INTERVAL_US) {//调用间隔大于50mstry {//获取底层的延迟,-bufferSizeUs,排除缓冲区造成的延迟(留下混音器和音频硬件驱动程序造成的延迟)latencyUs =castNonNull((Integer) getLatencyMethod.invoke(checkNotNull(audioTrack))) * 1000L- bufferSizeUs;// Check that the latency is non-negative.latencyUs = max(latencyUs, 0);// Check that the latency isn't too large.if (latencyUs > MAX_LATENCY_US) {listener.onInvalidLatency(latencyUs);latencyUs = 0;}} catch (Exception e) {// The method existed, but doesn't work. Don't try again.getLatencyMethod = null;}lastLatencySampleTimeUs = systemTimeUs;}}
Exoplayer 使用了2种方式来获取音轨的当前位置时间戳,在API19及以上,优先使用AudioTrack.getTimestamp来获取位置时间戳,否则采用AudiaTrack.getPlaybackHeadPosition来获取,由于getPlaybackHeadPosition精度较低还会采用一个平滑算法,计算出一个平均值来优化getPlaybackHeadPosition的精度。
总结
Renderer作为一个重要的组件,相比MediaSource的讲解可能比较简略,一方面因为Renderer的整体结构比MediaSource简单,没有分太多层,代码也比较集中。但这并不意味着不重要,这些代码值得仔细研究,其实这短短代码中蕴含着开发人员无数次的尝试调优,以及针对线上遇到问题的巧妙解决方案,有些方案可能在我看来比较无奈当又比不可少。另一方面,Renderer底层将解析工作交给了Android的系统组件,如果想要追根溯源那又是另一个系列了。还有原因就是不能再写了网站的在线编辑器到这里每打一个字都要卡很久哈哈。
版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持
相关文章:
ExoPlayer架构详解与源码分析(15)——Renderer
系列文章目录 ExoPlayer架构详解与源码分析(1)——前言 ExoPlayer架构详解与源码分析(2)——Player ExoPlayer架构详解与源码分析(3)——Timeline ExoPlayer架构详解与源码分析(4)—…...
网络安全-等级保护制度介绍
一、等保发展历程 (1)1994国务院147号令 第一次提出等级保护概念,要求对信息系统分等级进行保护 (2)1999年GB17859 国家强制标准发布,信息系统等级保护必须遵循的法规 (3)2005年公安…...
【介绍下大数据组件之Storm】
🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…...
React Hook 总结(React 萌新升级打怪中...)
1 useCallback useMemo 和 useCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果,useCallback 返回的是函数。 当需要使用 useCallback 的情况通常包括以…...
Typora 1.5.8 版本安装下载教程 (轻量级 Markdown 编辑器),图文步骤详解,免费领取
文章目录 软件介绍软件下载安装步骤激活步骤 软件介绍 Typora是一款基于Markdown语法的轻量级文本编辑器,它的主要目标是为用户提供一个简洁、高效的写作环境。以下是Typora的一些主要特点和功能: 实时预览:Typora支持实时预览功能࿰…...
mac docker no space left on device
mac 上 docker 拉取镜像报错 Error response from daemon: write /var/lib/docker/tmp/docker-export-3995807640/b8464f52498789c4ebbc063d508f04e8d2586567fbffa475e3cd9afd3c5a7cf2/layer.tar: no space left on device解决: 增加 docker 虚拟磁盘大小。如下图...
单片机主控的基本电路
论文 1.复位电路 2.启动模式设置接口 3.VBAT供电接口 4.MCU 基本电路 5.参考电压选择端口...
【19】读感 - 架构整洁之道(一)
概述 《架构整洁之道》一书中有提到设计和架构的感念,它们究竟是什么?书是这么说的,它们的层次不一样,架构更“高层级”的说法,这类讨论一般都把“底层”的实现细节排除在外。而设计往往指代的具体的系统底层组织结构…...
多层全连接神经网络(三)---分类问题
问题介绍 机器学习中的监督学习主要分为回归问题和分类问题,我们之前已经讲过回归问题了,它希望预测的结果是连续的,那么分类问题所预测的结果就是离散的类别。这时输入变量可以是离散的,也可以是连续的,而监督学习从数…...
签名优化:请求数据类型不是`application/json`,将只对随机数进行签名计算,例如文件上传接口。
文章目录 I 签名进行请求数据类型类型判断1.1 常见的ContentType1.2 签名切面处理1.3 文件上传案例1.4 处理接口信息背景: 文件上传接口的请求数据类型通常为multipart/form-data,方便携带文本域和使用接口文档进行调试。 如果携带JSON数据,不方便调试接口。 前端数据也要特…...
PostgreSQL的Json数据类型如何使用
PostgreSQL中的JSON数据类型提供了一种灵活的方式来存储JSON(JavaScript Object Notation)数据。JSON是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。在PostgreSQL中,你可以使用JSON和JSONB&…...
SpringData JPA Mongodb 查询部分字段
JPA 网上用的好像不多,找了好多材料以后最终找了这个可行的方案: Query(fields "{tender_id:1,_id:0}")List<MGPltTender> findByTenderIdIsNotNull(PageRequest pageRequest); 调用: Sort sort Sort.by(popularType.getC…...
NC65 设置下拉列表框值
NC65 设置下拉列表框值,如人员任职信息的异动事件: // 只有在入职登记时,才为异动事件下拉框过滤掉【离职】和【离职后变动】两个item DefaultConstEnum[] enumItems initTransevent(); BillItem item getBillCardPanel().getHeadItem(Psn…...
小阿轩yx-高性能内存对象缓存
小阿轩yx-高性能内存对象缓存 案例分析 案例概述 Memcached 是一款开源的高性能分布式内存对象缓存系统用于很多网站提高访问速度,尤其是需要频繁访问数据的大型网站是典型的 C/S 架构,需要构建 Memcached 服务器端与 Memcached API 客户端用 C 语言…...
华中师范大学学报人文社会科学版
一、《华中师范大学学报(人文社会科学版)》是国家教育部主管、华中师范大学主办的人文社会科学综合性学术期刊。本刊用稿以质量为标准,不分内稿外稿。文稿一经发表,即付报酬,不收版面费。 二、根据教育部和新闻出版总署颁发的社会科学学报编排规范,来稿应注意以下各项: 1. 题…...
CI/CD的node.js编译报错npm ERR! network request to https://registry.npmjs.org/
1、背景: 在维护paas云平台过程中,有研发反馈paas云平台上的CI/CD的前端流水线执行异常。 2、问题描述: 流水线执行的是前端编译,使用的是node.js环境。报错内容如下: 2024-07-18T01:23:04.203585287Z npm ERR! code E…...
用ssh tunnel的方式设置 AWS DocumentDB 公网访问
AWS DocumentDB的设定是只允许VPC内进行访问的,同时官方文档给了步骤,通过ssh tunnel的方式,可以从公网,或者从VPC外的网络,对DocumentDB进行访问。 我阅读了AWS官方文档并测试了这个步骤,如下是详细的步骤…...
基于电鸿(电力鸿蒙)的边缘计算网关,支持定制
1 产品信息 边缘计算网关基于平头哥 TH1520 芯片,支持 OpenHarmony 小型系统,是 连接物联网设备和云平台的重要枢纽,可应用于城市基础设施,智能工厂,智能建筑,营业网点,运营 服务中心相关场…...
WPF之URI的使用
pack://application:, pack://application:, 是一个在 WPF (Windows Presentation Foundation) 应用程序中用于指定资源位置的 URI (统一资源标识符) 方案的特定格式。这个格式用于访问嵌入在应用程序程序集(assemblies)中的资源,如图像、XA…...
Web开发:ASP.NET CORE前后端交互之AJAX(含基础Demo)
目录 一、后端 二、前端 三、代码位置 四、实现效果 五、关键的点 1.后端传输给前端: 2.前端传输给后端 一、后端 using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using WebAppl…...
经典神经网络(14)T5模型原理详解及其微调(文本摘要)
经典神经网络(14)T5模型原理详解及其微调(文本摘要) 2018 年,谷歌发布基于双向 Transformer 的大规模预训练语言模型 BERT,而后一系列基于 BERT 的研究工作如春笋般涌现,预训练模型也成为了业内解决 NLP 问题的标配。 2019年,谷歌…...
C语言结构体字节对齐技术详解
C语言结构体字节对齐技术详解(第一部分) 在C语言中,结构体字节对齐是一个重要的概念,它涉及到内存中数据的布局和访问效率。字节对齐可以帮助提高程序的性能,减少内存碎片,并确保数据的一致性和正确性。本…...
Linux编辑器——vim的使用
目录 vim的基本概念 命令模式 底行模式 插入模式 注释和取消注释 普通用户进行sudo提权 vim配置问题 vim的基本概念 一般使用的vim有三种模式: 命令模式 底行模式和插入模式,可以进行转换; vim filename 打开vim,进入的…...
Java案例斗地主游戏
目录 一案例要求: 二具体代码: 一案例要求: (由于暂时没有学到通信知识,所以只会发牌,不会设计打牌游戏) 二具体代码: Ⅰ:主函数 package three;public class test {…...
sqlite|轻量数据库|pgadmin4的sqlite数据库操作--重置密码和账号解锁
前言: pgadmin4的用户密码以及pgadmin4创建的pg数据库的连接信息等等都是存放在sqlite数据库内的;而有的时候,可能会由于自己的问题将pgadmin4的密码忘记,这个时候需要重置pgadmin4的密码,或者是pgadmin4的密码输错多…...
【ARMv8/v9 异常模型入门及渐进 9.1 - FIQ 和 IRQ 打开和关闭】
请阅读【ARMv8/v9 ARM64 System Exception】 文章目录 FIQ/IRQ Enable and Disable汇编指令详解功能解释使用场景和注意事项 FIQ/IRQ Enable and Disable 在ARMv8/v9架构中,可以使用下面汇编指令来打开FIQ和 IRQ,代码如下: asm volatile ("msr da…...
深入探索Flutter中的状态管理:使用Provider库
当涉及Flutter状态管理时,provider是一个强大且灵活的解决方案,它提供了一种简单且高效的方式来管理应用程序状态。本文将详细介绍Flutter中provider插件的使用方法、示例代码、各种使用场景以及注意事项。 1. 引入依赖 首先,需要在项目的pubspec.yaml文件中添加provider依…...
算法工程师第十四天(找树左下角的值 路径总和 从中序与后序遍历序列构造二叉树 )
参考文献 代码随想录 一、找树左下角的值 给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,6,null,null,7] 输出: 7 层次遍历&#…...
memcached 高性能内存对象缓存
memcached 高性能内存对象缓存 memcache是一款开源的高性能分布式内存对象缓存系统,常用于做大型动态web服务器的中间件缓存。 mamcached做web服务的中间缓存示意图 当web服务器接收到请求需要处理动态页面元素时,通常要去数据库调用数据,但…...
C语言 分割链表
题目来源: 代码部分,参考官方题解的写法: // 思路: 就是把原始链表,拆分为2部分,最后再拼接一下。struct ListNode* partition(struct ListNode* head, int x) {struct ListNode* small malloc(sizeof(struct ListNode));struct ListNode*…...
网站开发程序有哪些/四川seo平台
USE [master] GO ALTER DATABASE A_DB SET RECOVERY SIMPLE WITH NO_WAIT --(No_WAIT : 指定如果请求的数据库语句或选项更改只有等待事务自主提交或回滚才能立即完成,该请求将失败。) GO ALTER DATABASE A_DB SET RECOVERY SIMPLE --(S…...
营销型网站建设的费用报价单/中国优化网
react-redux使用小结 react-reduxstorereduceraction整合storereduceraction补充 使用redux-dev-tools让改变reducer后能够即时刷新页面总结需要使用的库redux,react-redux,react-router-redux react-redux 使用一个react-redux 的库使得redux的使用更…...
公司做网站需要备案吗/想在百度做推广怎么做
第一种:使用伪元素transform:translateX(-100%);原理是设置文本居中,给定两个伪元素,分别绝对定位,那么此时伪元素也是跟随着水平居中的,设置的宽度,然后把左边的往左位移100%就可以了,父元素超…...
合肥网页网站制作/为企业推广
利用Servlet3.0能够轻松写出一个博客系统,主要是把数据存入数据库与从数据库读出数据怎么推向前台的问题。本章将会以一个博客系统来说明,如何利用JSTL表达式把Servlet从数据库读出的数据推向前台。将会利用网页文本格式编辑器xheditor与EL表达式基于Ser…...
网站建设中首页模板下载/精准推广的渠道有哪些
文章目录1. 可变数据2. 发散式变化1. 可变数据 定义:对数据的修改经常导致出乎意料的结果和难以发现的bug。 影响:在一处修改数据,却在另一处造成难以发现的破坏,影响可维护性。 重构目标:应用数据不变性࿰…...
wordpress文章自动发布/网站建设小程序开发
由于 TensorFlow版本差异,经常会报模块对象没有某属性的错误,先把tensorboard可视化过程遇到的几个报错解决方案记录如下:1. AttributeError: module object has no attribute histogram_summaryhistogram_summary 改为:tf.summar…...