一个视频由声音轨道和视频轨道组成,一般声音轨道的数据比较小,我们一般不需要担心声音解析的问题.但是视频轨道的数据很大,视频轨道的解码就是整个视频解码的瓶颈。
Render执行流程:
音频和视频解码默认都是使用的MediaCodec,视频解码放在Render:MediaCodecVideoRenderer中完成,音频解码放在MediaCodecAudioRenderer中完成。
- 1.MediaCodec有同步和异步两种方式,ExoPlayer中使用的是同步还是异步的方式? ExoPlayer中使用的是同步的方式,同步的方式不用设置MediaCodec.setCallback(....)回调,依赖codec.dequeueInputBuffer和codec.dequeueOutputBuffer获取可用的input buffer和output buffer。 这里的input buffer就是不断从Codec中取出解码好的数据,然后放到output buffer中,送出去显示出来。
- 2.核心的处理方法是MediaCodecRenderer中的render函数:
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (pendingOutputEndOfStream) {
pendingOutputEndOfStream = false;
processEndOfStream();
}
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.
maybeInitCodec();
if (codec != null) {
long drainStartTimeMs = SystemClock.elapsedRealtime();
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {}
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(e, inputFormat);
}
throw e;
}
}
maybeInitCodec根据mimetype创建codec,然后将surface配置到codec内部,之后的output buffers数据会不断送显到这个surface上. drainOutputBuffer消耗codec中的output buffers队列中的数据; feedInputBuffer填充codec中的input buffers队列.
shouldContinueFeeding(drainStartTimeMs)函数一看是作音视频同步用的,下一章我们分析音视频同步,本章我们只将音视频解码的流程以及其中的关键点. 关于丢帧的讨论都在音视频同步中分析
视频的送显直接使用在MediaCodec.configure中设置surface就可以的,音频的播放还是要借助AudioTrack或者OpenSL ES,当然ExoPlayer中使用的是AudioTrack
音频播放
初始化定义Audio Render的时候已经设置了Audio的渲染入口: DefaultRenderersFactory中buildAudioRenderers 函数
out.add(
new MediaCodecAudioRenderer(
context,
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
enableDecoderFallback,
eventHandler,
eventListener,
new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
关注最后一个参数DefaultAudioSink对象. AudioProcessor是处理音频的基类,拓展他实现众多音频处理的方式. DefaultAudioSink.initialize函数中初始化一个AudioTrack对象:
audioTrack =
Assertions.checkNotNull(configuration)
.buildAudioTrack(tunneling, audioAttributes, audioSessionId);
在这个函数的下面有一个AudioTrackPositionTracker对象,这个对象是音频同步的辅助类,非常重要.
audioTrackPositionTracker.setAudioTrack(
audioTrack,
configuration.outputEncoding,
configuration.outputPcmFrameSize,
configuration.bufferSize);
MediaCodecAudioRenderer.processOutputBuffer中将解码出来的原始音频数据送到AudioSink中渲染:这是一个循环的过程,不断地从output buffers中去除解码出的原始数据.
if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) {
codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.renderedOutputBufferCount++;
return true;
}
DefaultAudioSink.handleBuffer中处理的核心方法块是:
if (configuration.processingEnabled) {
processBuffers(presentationTimeUs);
} else {
writeBuffer(inputBuffer, presentationTimeUs);
}
将解码出来的原始数据写道AudioTrack中:
if (Util.SDK_INT < 21) { // isInputPcm == true
// Work out how many bytes we can write without the risk of blocking.
int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes);
if (bytesToWrite > 0) {
bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
if (bytesWritten > 0) {
preV21OutputBufferOffset += bytesWritten;
buffer.position(buffer.position() + bytesWritten);
}
}
} else if (tunneling) {
Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining,
avSyncPresentationTimeUs);
} else {
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
}
audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
AudioTrack的阻塞类型都是WRITE_NON_BLOCKING,不能影响播放线程的进行
AudioTrack写入buffer的时候会明确告知系统当前的buffer的timestamp,在播放时候会根据这个timestamp来控制播放的进度.
- ExoPlayer中关于Render这一块很多工业代码,但是不妨碍我们对整体流程的把握
- 音视频这一块的核心功能还是音视频同步
问题: 如何引入其他的Audio或者Video Render模块? 请思考一下,以后揭晓