基于ExoPlayer 2.17.1源码分析,分析下音视频数据是如何到播放器播放的:
首先话接上上篇ExoPlayer 源码阅读小记–HLS播放带缓存加载M38U文件过程最后一步
MediaChunk执行chunk加载,首先通过prepareExtraction创建出input数据流和HlsMediaChunkExtractor执行器,然后通过执行器执行相应的流,流的这块后续处理还是比较复杂的不是本文重点,主要目的就是ts文件的Demux,解析出Demux(对应代码里的Sample,相当于一帧)后的数据发送到一个DataQuene里供Renderer获取,Renderer最终调用系统的MediaCodec渲染到surface上
//HlsMediaChunk
private void feedDataToExtractor(
DataSource dataSource,
DataSpec dataSpec,
boolean dataIsEncrypted,
boolean initializeTimestampAdjuster)
throws IOException {
....
try {
ExtractorInput input =
prepareExtraction(dataSource, loadDataSpec, initializeTimestampAdjuster);
if (skipLoadedBytes) {
input.skipFully(nextLoadPosition);
}
try {
while (!loadCanceled && extractor.read(input)) {}//通过执行器执行相应的流
} catch (EOFException e) {
....
}
private DefaultExtractorInput prepareExtraction(
DataSource dataSource, DataSpec dataSpec, boolean initializeTimestampAdjuster)
throws IOException {
long bytesToRead = dataSource.open(dataSpec);//这里又看到熟悉的代码类似ParsingLoadable加载m3u8文件一样,调用我们的三通TeeDataSource从网络获取ts文件,数据流会保存在datasource等待读取,读取时缓存文件
....
DefaultExtractorInput extractorInput =
new DefaultExtractorInput(dataSource, dataSpec.position, bytesToRead);//数据流
if (extractor == null) {
....
extractor =//执行器
previousExtractor != null
? previousExtractor.recreate()
: extractorFactory.createExtractor(
dataSpec.uri,
trackFormat,
muxedCaptionFormats,
timestampAdjuster,
dataSource.getResponseHeaders(),
extractorInput,
playerId);
....
return extractorInput;
}
现在我们来详细分析下上面说的Renderer过程,继续往下看extractor read的过程,这里我们播放是ts文件,这个个read最终会使用TsExtractor的reader,这里主要就是对TS文件的解析,具体需要参照TS文件的结构看代码,这里就不在赘述,代码每次读取ts的一个块也就是188bytes,然后分析头和payload,继续看payload部分payloadReader.consume
//TsExtractor
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
long inputLength = input.getLength();
....
if (!fillBufferWithAtLeastOnePacket(input)) {
return RESULT_END_OF_INPUT;
}
....
TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null;
....
// Read the payload.
boolean wereTracksEnded = tracksEnded;
if (shouldConsumePacketPayload(pid)) {
tsPacketBuffer.setLimit(endOfPacket);
payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
tsPacketBuffer.setLimit(limit);
}
....
return RESULT_CONTINUE;
}
首次payloadReader get不到会使用SectionReader consume分析段信息,通过PatReader或者PmtReader 读取PAT和PMT表,最终目的就是查找出音视频数据具体参考TS文件个格式,这里我们直接进入到视频数据的读取,由于我们是采用的H.264标准的视频,最终会通过PesReader consume 调用H264Reader 的 consume 方法
//PesReader
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called.
if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
switch (state) {
....
while (data.bytesLeft() > 0) {
switch (state) {
case STATE_FINDING_HEADER:
data.skipBytes(data.bytesLeft());
break;
case STATE_READING_HEADER:
if (continueRead(data, pesScratch.data, HEADER_SIZE)) {
setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);
}
break;
case STATE_READING_HEADER_EXTENSION:
int readLength = min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);
// Read as much of the extended header as we're interested in, and skip the rest.
if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, /* target= */ null, extendedHeaderLength)) {
parseHeaderExtension();
flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
reader.packetStarted(timeUs, flags);
setState(STATE_READING_BODY);
}
break;
case STATE_READING_BODY:
readLength = data.bytesLeft();
int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize;
if (padding > 0) {
readLength -= padding;
data.setLimit(data.getPosition() + readLength);
}
reader.consume(data);//读取body
if (payloadSize != C.LENGTH_UNSET) {
payloadSize -= readLength;
if (payloadSize == 0) {
reader.packetFinished();
setState(STATE_READING_HEADER);
}
}
break;
default:
throw new IllegalStateException();
}
}
}
H264Reader 会通过output将数据加到SampleDataQueue的buffer里,另外还会扫描附加数据,处理AL单元,获取当前视频帧的数据信息
//H264Reader
public void consume(ParsableByteArray data) {
assertTracksCreated();
int offset = data.getPosition();
int limit = data.limit();
byte[] dataArray = data.getData();
// Append the data to the buffer.
totalBytesWritten += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
// Scan the appended data, processing NAL units as they are encountered
while (true) {
int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nalUnitOffset == limit) {
// We've scanned to the end of the data without finding the start of another NAL unit.
nalUnitData(dataArray, offset, limit);
return;
}
// We've seen the start of a NAL unit of the following type.
int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);
// This is the number of bytes from the current offset to the start of the next NAL unit.
// It may be negative if the NAL unit started in the previously consumed data.
int lengthToNalUnit = nalUnitOffset - offset;
if (lengthToNalUnit > 0) {
nalUnitData(dataArray, offset, nalUnitOffset);
}
int bytesWrittenPastPosition = limit - nalUnitOffset;
long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
// Indicate the end of the previous NAL unit. If the length to the start of the next unit
// is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
// when notifying that the unit has ended.
endNalUnit(
absolutePosition,
bytesWrittenPastPosition,
lengthToNalUnit < 0 ? -lengthToNalUnit : 0,
pesTimeUs);
// Indicate the start of the next NAL unit.
startNalUnit(absolutePosition, nalUnitType, pesTimeUs);
// Continue scanning the data.
offset = nalUnitOffset + 3;
}
}
最终通过output的sampleMetadata写入每个Sample的timeUs,size,offset,cryptoData等信息,SampleQueue内部管理着SampleDataQueue(也就是视频的帧数据),SharedSampleMetadata 、offsets、sizes、flags、timesUs(数据对应的Metadata信息),具体关系在下面读取的时候会讲到。
//SampleQueue
public void sampleMetadata(
long timeUs,
@C.BufferFlags int flags,
int size,
int offset,
@Nullable CryptoData cryptoData) {
if (upstreamFormatAdjustmentRequired) {
format(Assertions.checkStateNotNull(unadjustedUpstreamFormat));
}
boolean isKeyframe = (flags & C.BUFFER_FLAG_KEY_FRAME) != 0;
if (upstreamKeyframeRequired) {
if (!isKeyframe) {
return;
}
upstreamKeyframeRequired = false;
}
timeUs += sampleOffsetUs;
if (upstreamAllSamplesAreSyncSamples) {
if (timeUs < startTimeUs) {
// If we know that all samples are sync samples, we can discard those that come before the
// start time on the write side of the queue.
return;
}
if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
// The flag should always be set unless the source content has incorrect sample metadata.
// Log a warning (once per format change, to avoid log spam) and override the flag.
if (!loggedUnexpectedNonSyncSample) {
Log.w(TAG, "Overriding unexpected non-sync sample for format: " + upstreamFormat);
loggedUnexpectedNonSyncSample = true;
}
flags |= C.BUFFER_FLAG_KEY_FRAME;
}
}
if (pendingSplice) {
if (!isKeyframe || !attemptSplice(timeUs)) {
return;
}
pendingSplice = false;
}
long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset;
commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
}
最终调用SampleDataQueue的sampleData,首先介绍下SampleDataQueue,首先它是一个队列,确切的说应该是它内部维护了一个队列,队列每一项都是一个AllocationNode,AllocationNode有个next属性用来指向下一项,这个队列是滚动的,队尾不停的放入新的数据也就是下面的sampleData
//SampleDataQueue
public void sampleData(ParsableByteArray buffer, int length) {
while (length > 0) {
int bytesAppended = preAppend(length);
buffer.readBytes(
writeAllocationNode.allocation.data,
writeAllocationNode.translateOffset(totalBytesWritten),
bytesAppended);
length -= bytesAppended;
postAppend(bytesAppended);
}
}
private int preAppend(int length) {
if (writeAllocationNode.allocation == null) {//分配空间当前Node空间,并实列化下一个Node
writeAllocationNode.initialize(
allocator.allocate(),//DefaultAllocator 分配空间
new AllocationNode(writeAllocationNode.endPosition, allocationLength));
}
return min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
}
private void postAppend(int length) {
totalBytesWritten += length;//更新已写大小
if (totalBytesWritten == writeAllocationNode.endPosition) {//如果当前Node写满了,将当前写入Node指向下一个
writeAllocationNode = writeAllocationNode.next;
}
}
这里可以了解下DefaultAllocator这个类,这是一个内存buffer分配器,负责分配和管理内存,当已分配的内存不在使用时(如已经被Renderer读取)播放器在dosomework 内部loop时会释放掉这块内存,最终会调用release“释放掉”,但是并不是正在意义上的释放只是将当前内存块放入到availableAllocations 这个List里保存,供allocate时重复循环利用,这么看SampleDataQueue也可以理解成一个环形队列。另外我们看到这个DefaultAllocator在内存分配的时候并没有做限制,当分配内存已经超过APP最大可用内存时这里就会OOM。
//DefaultAllocator
public synchronized Allocation allocate() {
allocatedCount++;
Allocation allocation;
if (availableCount > 0) {//如果当前还有可用内存,使用当前
allocation = Assertions.checkNotNull(availableAllocations[--availableCount]);
availableAllocations[availableCount] = null;
} else {//没有足够的剩余内存则创建并扩容List大小为原先2倍
allocation = new Allocation(new byte[individualAllocationSize], 0);
if (allocatedCount > availableAllocations.length) {
// Make availableAllocations be large enough to contain all allocations made by this
// allocator so that release() does not need to grow the availableAllocations array. See
// [Internal ref: b/209801945].
availableAllocations = Arrays.copyOf(availableAllocations, availableAllocations.length * 2);
}
}
return allocation;
}
看完写入我们再来看读取,读取要从播放器内部loop dosomework看起,这是一个很重要的方法,贯穿了播放器的整个播放过程,这里我们做个分支任务详细分析下这个方法,主要关注updatePeriods方法
//ExoPlayerImplInternal
private void doSomeWork() throws ExoPlaybackException, IOException {
long operationStartTimeMs = clock.uptimeMillis();
updatePeriods();//更新Periods
if (playbackInfo.playbackState == Player.STATE_IDLE
|| playbackInfo.playbackState == Player.STATE_ENDED) {
// Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
handler.removeMessages(MSG_DO_SOME_WORK);
return;
}
@Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
if (playingPeriodHolder == null) {
// We're still waiting until the playing period is available.
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
return;
}
TraceUtil.beginSection("doSomeWork");
updatePlaybackPositions();
boolean renderersEnded = true;
boolean renderersAllowPlayback = true;
if (playingPeriodHolder.prepared) {
long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
playingPeriodHolder.mediaPeriod.discardBuffer(
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
if (!isRendererEnabled(renderer)) {
continue;
}
// TODO: Each renderer should return the maximum delay before which it wishes to be called
// again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
renderersEnded = renderersEnded && renderer.isEnded();
// Determine whether the renderer allows playback to continue. Playback can continue if the
// renderer is ready or ended. Also continue playback if the renderer is reading ahead into
// the next stream or is waiting for the next stream. This is to avoid getting stuck if
// tracks in the current period have uneven durations and are still being read by another
// renderer. See: https://github.com/google/ExoPlayer/issues/1874.
boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
boolean allowsPlayback =
isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
if (!allowsPlayback) {
renderer.maybeThrowStreamError();
}
}
} else {
playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
}
....
}
private void updatePeriods() throws ExoPlaybackException, IOException {
if (playbackInfo.timeline.isEmpty() || !mediaSourceList.isPrepared()) {
// No periods available.
return;
}
maybeUpdateLoadingPeriod();
maybeUpdateReadingPeriod();
maybeUpdateReadingRenderers();
maybeUpdatePlayingPeriod();
}
看下maybeUpdateLoadingPeriod,首先判断是否需要下一曲,然后判断是否需要继续加载下一个ts文件
//ExoPlayerImplInternal
private void maybeUpdateLoadingPeriod() throws ExoPlaybackException {
queue.reevaluateBuffer(rendererPositionUs);//上面说的释放掉一些已读的Allocation内存块
if (queue.shouldLoadNextMediaPeriod()) {//是否需要加载下一个MediaPeriod,根据Timeline定义,这种属于Single media file,包含一个Period,如果是一个播放列表,就对应下一个曲目
@Nullable
MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
if (info != null) {
MediaPeriodHolder mediaPeriodHolder =
queue.enqueueNextMediaPeriodHolder(
rendererCapabilities,
trackSelector,
loadControl.getAllocator(),
mediaSourceList,
info,
emptyTrackSelectorResult);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
if (queue.getPlayingPeriod() == mediaPeriodHolder) {
resetRendererPosition(info.startPositionUs);
}
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
}
}
if (shouldContinueLoading) {
// We should still be loading, except when there is nothing to load or we have fully loaded
// the current period.
shouldContinueLoading = isLoadingPossible();
updateIsLoading();
} else {
maybeContinueLoading();//决定是否需要继续加载下一个分段到内存,这里对应HLS的一个ts文件
}
}
private void maybeContinueLoading() {
shouldContinueLoading = shouldContinueLoading();
if (shouldContinueLoading) {
queue.getLoadingPeriod().continueLoading(rendererPositionUs);//对应前篇末尾所说的continueLoading
}
updateIsLoading();
}
private boolean shouldContinueLoading() {
if (!isLoadingPossible()) {
return false;
}
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
long bufferedDurationUs =
getTotalBufferedDurationUs(loadingPeriodHolder.getNextLoadPositionUs());
long playbackPositionUs =
loadingPeriodHolder == queue.getPlayingPeriod()
? loadingPeriodHolder.toPeriodTime(rendererPositionUs)
: loadingPeriodHolder.toPeriodTime(rendererPositionUs)
- loadingPeriodHolder.info.startPositionUs;
return loadControl.shouldContinueLoading(//调用loadcontrol来判断
playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
}
其中判断是否加载下一个的逻辑在DefaultLoadControl里,这个可以在创建ExoPlayer时通过setLoadControl方法设置,
这个类主要关注4个属性:
- targetBufferBytes 最大可用缓存,如果设置为-1,会动态通过当前缓存的轨道类型设置合适的大小,如果是视频轨则大小为2000 * 64*1024 bytes
- minBufferUs 当前缓存的最小时长,单位微秒
- maxBufferUs 当前缓存的最大时长,单位微秒
- prioritizeTimeOverSizeThresholds 是否采用时间优先,设置为true会忽略targetBufferBytes 最大可用缓存
用户可以通过设置以上几个值来制定自己的缓存策略,注意这里说的缓存指的是存储内存中视频的数据,如果设置不当很有可能OOM
//DefaultLoadControl
public boolean shouldContinueLoading(
long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes;//判断是否超出最大允许内存,这里获取所有已分配内存使用的就是上面说的DefaultAllocator
long minBufferUs = this.minBufferUs;
if (playbackSpeed > 1) {//倍数播放的场景需要重新计算最小缓存时间
// The playback speed is faster than real time, so scale up the minimum required media
// duration to keep enough media buffered for a playout duration of minBufferUs.
long mediaDurationMinBufferUs =
Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed);
minBufferUs = min(mediaDurationMinBufferUs, maxBufferUs);
}
// Prevent playback from getting stuck if minBufferUs is too small.
minBufferUs = max(minBufferUs, 500_000);//最小不能低于500000
if (bufferedDurationUs < minBufferUs) {//比较时间
isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;//决定是否需要缓存,优先判断时间优先级,时间优先级高的化就直接忽略targetBufferBytes了
if (!isLoading && bufferedDurationUs < 500_000) {
Log.w(
"DefaultLoadControl",
"Target buffer size reached with less than 500ms of buffered media data.");
}
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isLoading = false;
} // Else don't change the loading state.
return isLoading;
}
继续看doSomeWork renderer.render 这里又回到我们主线任务来了,这里是使用renderer 来渲染数据到屏幕,应该就是读取数据的入口的了,数据的解码方式有很多,EXO默认是采用系统来解码的,也就是是MediaCodec,这里直接看MediaCodecRenderer的render
//MediaCodecRenderer
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
....
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
&& shouldContinueRendering(renderStartTimeMs)) {}
while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {}//循环读取
TraceUtil.endSection();
....
}
private boolean feedInputBuffer() throws ExoPlaybackException {
....
@SampleStream.ReadDataResult int result;
try {
result = readSource(formatHolder, buffer, /* readFlags= */ 0);
} catch (InsufficientCapacityException e) {
onCodecError(e);
// Skip the sample that's too large by reading it without its data. Then flush the codec so
// that rendering will resume from the next key frame.
readSourceOmittingSampleData(/* readFlags= */ 0);
flushCodec();
return true;
}
....
}
protected final @ReadDataResult int readSource(
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
@ReadDataResult
int result = Assertions.checkNotNull(stream).readData(formatHolder, buffer, readFlags);
....
return result;
}
//HlsSampleStream
public int readData(
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
....
return hasValidSampleQueueIndex()
? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, readFlags)
: C.RESULT_NOTHING_READ;
}
//HlsSampleStreamWrapper
public int readData(
int sampleQueueIndex,
FormatHolder formatHolder,
DecoderInputBuffer buffer,
@ReadFlags int readFlags) {
....
int result =
sampleQueues[sampleQueueIndex].read(formatHolder, buffer, readFlags, loadingFinished);
....
return result;
}
SampleQueue读取数据时并不是直接从sampleDataQueue中读取,而是先调用peekSampleMetadata给buffer设置Metadata相关数据,然后再去从sampleDataQueue里获取帧数据,看下peekSampleMetadata方法,我们发现它是通过synchronized加锁的,而sampleDataQueue的peekToBuffer并没有加锁,到这里应该会理解为什么会先读取Metadata了
//SampleQueue
public int read(
FormatHolder formatHolder,
DecoderInputBuffer buffer,
@ReadFlags int readFlags,
boolean loadingFinished) {
int result =
peekSampleMetadata(
formatHolder,
buffer,
/* formatRequired= */ (readFlags & FLAG_REQUIRE_FORMAT) != 0,
loadingFinished,
extrasHolder);
if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
boolean peek = (readFlags & FLAG_PEEK) != 0;
if ((readFlags & FLAG_OMIT_SAMPLE_DATA) == 0) {
if (peek) {
sampleDataQueue.peekToBuffer(buffer, extrasHolder);
} else {
sampleDataQueue.readToBuffer(buffer, extrasHolder);
}
}
if (!peek) {
readPosition++;
}
}
return result;
}
可以看到SampleData和SampleMetadata之间通过relativeReadIndex来关联,读取数据时先读取SampleMetadata,计算出relativeReadIndex获取到SampleData offset,用于后面的SampleData获取
//SampleQueue
private synchronized int peekSampleMetadata(
FormatHolder formatHolder,
DecoderInputBuffer buffer,
boolean formatRequired,
boolean loadingFinished,
SampleExtrasHolder extrasHolder) {
buffer.waitingForKeys = false;
if (!hasNextSample()) {
if (loadingFinished || isLastSampleQueued) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) {
onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder);
return C.RESULT_FORMAT_READ;
} else {
return C.RESULT_NOTHING_READ;
}
}
Format format = sharedSampleMetadata.get(getReadIndex()).format;
if (formatRequired || format != downstreamFormat) {
onFormatResult(format, formatHolder);
return C.RESULT_FORMAT_READ;
}
int relativeReadIndex = getRelativeIndex(readPosition);//通过计算relativeReadIndex获取对应的Metadata信息
if (!mayReadSample(relativeReadIndex)) {
buffer.waitingForKeys = true;
return C.RESULT_NOTHING_READ;
}
buffer.setFlags(flags[relativeReadIndex]);
buffer.timeUs = timesUs[relativeReadIndex];
if (buffer.timeUs < startTimeUs) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
extrasHolder.size = sizes[relativeReadIndex];
extrasHolder.offset = offsets[relativeReadIndex];//将offset保存到holder中供后面读取SampData用
extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];
return C.RESULT_BUFFER_READ;
}
//SampleDataQueue
public void peekToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
readSampleData(readAllocationNode, buffer, extrasHolder, scratch);
}
private static AllocationNode readSampleData(
AllocationNode allocationNode,
DecoderInputBuffer buffer,
SampleExtrasHolder extrasHolder,
ParsableByteArray scratch) {
....
// Write the sample data.
buffer.ensureSpaceForWrite(sampleSize);
allocationNode = readData(allocationNode, extrasHolder.offset, buffer.data, sampleSize);
extrasHolder.offset += sampleSize;
extrasHolder.size -= sampleSize;
....
return allocationNode;
}
private static AllocationNode readData(
AllocationNode allocationNode, long absolutePosition, ByteBuffer target, int length) {
allocationNode = getNodeContainingPosition(allocationNode, absolutePosition);
int remaining = length;
while (remaining > 0) {
int toCopy = min(remaining, (int) (allocationNode.endPosition - absolutePosition));
Allocation allocation = allocationNode.allocation;
target.put(allocation.data, allocationNode.translateOffset(absolutePosition), toCopy);//获取allocation里的数据
remaining -= toCopy;
absolutePosition += toCopy;
if (absolutePosition == allocationNode.endPosition) {
allocationNode = allocationNode.next;
}
}
return allocationNode;
}
到这里数据的读取过程就结束了,最终renderer会将获取到的数据渲染到屏幕上
看完这些感觉Exoplayer这块设计得很巧妙,最大限度的保证了视频的读写效率:
- SampleQueue内部管理着SampleData,SampleMetadata,通过SampleMetadata来管理SampleData
SampleData读写是不加锁的,SampleData数据量大,这样可以保证视频数据读写的效率,
SampleMetadata读写是加锁的,但是metadata数据量太小了,这个锁基本不会影响视频数据读写的效率 - SampleData通过SampleDataQueue来管理,这是一个动态的Queue,使用多少分配多少空间,不用就会回收再利用,超出大小会自动扩容,比单纯的一块固定大小的buffer要灵活实用很多