Parse ID3 sample timestamp for HLS audio chunks

Pending improvement:

* Peek just the required priv frame. Avoid decoding all id3 information.
* Sniff the used container format instead of using the extension.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=141181781
This commit is contained in:
aquilescanta 2016-12-06 08:33:30 -08:00 committed by Oliver Woodman
parent e5bc00ea94
commit a66b4a9bad
2 changed files with 184 additions and 92 deletions

View File

@ -278,10 +278,9 @@ import java.util.Locale;
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
encryptionKey, encryptionIv);
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs,
startTimeUs + segment.durationUs, chunkMediaSequence, segment.discontinuitySequenceNumber,
isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv);
}
/**

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.source.hls;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
@ -26,11 +28,15 @@ import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
@ -42,6 +48,9 @@ import java.util.concurrent.atomic.AtomicInteger;
private static final AtomicInteger UID_SOURCE = new AtomicInteger();
private static final String PRIV_TIMESTAMP_FRAME_OWNER =
"com.apple.streaming.transportStreamTimestamp";
private static final String AAC_FILE_EXTENSION = ".aac";
private static final String AC3_FILE_EXTENSION = ".ac3";
private static final String EC3_FILE_EXTENSION = ".ec3";
@ -71,6 +80,11 @@ import java.util.concurrent.atomic.AtomicInteger;
private final boolean isMasterTimestampSource;
private final TimestampAdjuster timestampAdjuster;
private final HlsMediaChunk previousChunk;
private final String lastPathSegment;
private final boolean isPackedAudio;
private final Id3Decoder id3Decoder;
private final ParsableByteArray id3Data;
private Extractor extractor;
private int initSegmentBytesLoaded;
@ -113,6 +127,19 @@ import java.util.concurrent.atomic.AtomicInteger;
this.previousChunk = previousChunk;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
lastPathSegment = dataSpec.uri.getLastPathSegment();
isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION)
|| lastPathSegment.endsWith(AC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(EC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(MP3_FILE_EXTENSION);
if (isPackedAudio) {
id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder();
id3Data = previousChunk != null ? previousChunk.id3Data
: new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
} else {
id3Decoder = null;
id3Data = null;
}
initDataSource = dataSource;
adjustedEndTimeUs = endTimeUs;
uid = UID_SOURCE.getAndIncrement();
@ -167,8 +194,9 @@ import java.util.concurrent.atomic.AtomicInteger;
@Override
public void load() throws IOException, InterruptedException {
if (extractor == null) {
extractor = buildExtractor();
if (extractor == null && !isPackedAudio) {
// See HLS spec, version 20, Section 3.4 for more information on packed audio extraction.
extractor = buildExtractorByExtension();
}
maybeLoadInitData();
if (!loadCanceled) {
@ -176,27 +204,152 @@ import java.util.concurrent.atomic.AtomicInteger;
}
}
// Private methods.
// Internal loading methods.
private Extractor buildExtractor() {
private void maybeLoadInitData() throws IOException, InterruptedException {
if ((previousChunk != null && previousChunk.extractor == extractor) || initLoadCompleted
|| initDataSpec == null) {
return;
}
DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
try {
ExtractorInput input = new DefaultExtractorInput(initDataSource,
initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec));
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
} finally {
initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
}
initLoadCompleted = true;
}
private void loadMedia() throws IOException, InterruptedException {
// If we previously fed part of this chunk to the extractor, we need to skip it this time. For
// encrypted content we need to skip the data by reading it through the source, so as to ensure
// correct decryption of the remainder of the chunk. For clear content, we can request the
// remainder of the chunk directly.
DataSpec loadDataSpec;
boolean skipLoadedBytes;
if (isEncrypted) {
loadDataSpec = dataSpec;
skipLoadedBytes = bytesLoaded != 0;
} else {
loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
skipLoadedBytes = false;
}
if (!isMasterTimestampSource) {
timestampAdjuster.waitUntilInitialized();
}
try {
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (extractor == null) {
// Media segment format is packed audio.
long id3Timestamp = peekId3PrivTimestamp(input);
if (id3Timestamp == C.TIME_UNSET) {
throw new ParserException("ID3 PRIV timestamp missing.");
}
extractor = buildPackedAudioExtractor(timestampAdjuster.adjustTsTimestamp(id3Timestamp));
}
if (skipLoadedBytes) {
input.skipFully(bytesLoaded);
}
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
long adjustedEndTimeUs = extractorOutput.getLargestQueuedTimestampUs();
if (adjustedEndTimeUs != Long.MIN_VALUE) {
this.adjustedEndTimeUs = adjustedEndTimeUs;
}
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
/**
* Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined
* in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not
* found. This method only modifies the peek position.
*
* @param input The {@link ExtractorInput} to obtain the PRIV frame from.
* @return The parsed, adjusted timestamp in microseconds
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
if (!input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) {
return C.TIME_UNSET;
}
id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
int id = id3Data.readUnsignedInt24();
if (id != Id3Decoder.ID3_TAG) {
return C.TIME_UNSET;
}
id3Data.skipBytes(3); // version(2), flags(1).
int id3Size = id3Data.readSynchSafeInt();
int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH;
if (requiredCapacity > id3Data.capacity()) {
byte[] data = id3Data.data;
id3Data.reset(requiredCapacity);
System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
}
if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) {
return C.TIME_UNSET;
}
Metadata metadata = id3Decoder.decode(id3Data.data, id3Size);
if (metadata == null) {
return C.TIME_UNSET;
}
int metadataLength = metadata.length();
for (int i = 0; i < metadataLength; i++) {
Metadata.Entry frame = metadata.get(i);
if (frame instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) frame;
if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */);
id3Data.reset(8);
return id3Data.readLong();
}
}
}
return C.TIME_UNSET;
}
// Internal factory methods.
/**
* If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in
* order to decrypt the loaded data. Else returns the original.
*/
private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey,
byte[] encryptionIv) {
if (encryptionKey == null || encryptionIv == null) {
return dataSource;
}
return new Aes128DataSource(dataSource, encryptionKey, encryptionIv);
}
private Extractor buildExtractorByExtension() {
// Set the extractor that will read the chunk.
Extractor extractor;
boolean needNewExtractor = previousChunk == null
|| previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
|| trackFormat != previousChunk.trackFormat;
boolean usingNewExtractor = true;
String lastPathSegment = dataSpec.uri.getLastPathSegment();
if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
// TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner
// identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3
// case below.
extractor = new AdtsExtractor(startTimeUs);
} else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
extractor = new Ac3Extractor(startTimeUs);
} else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
extractor = new Mp3Extractor(startTimeUs);
} else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
|| lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster);
} else if (!needNewExtractor) {
@ -231,80 +384,20 @@ import java.util.concurrent.atomic.AtomicInteger;
return extractor;
}
private void maybeLoadInitData() throws IOException, InterruptedException {
if ((previousChunk != null && previousChunk.extractor == extractor)
|| initLoadCompleted || initDataSpec == null) {
return;
}
DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
try {
ExtractorInput input = new DefaultExtractorInput(initDataSource,
initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec));
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
} finally {
initSegmentBytesLoaded += (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
}
initLoadCompleted = true;
}
private void loadMedia() throws IOException, InterruptedException {
// If we previously fed part of this chunk to the extractor, we need to skip it this time. For
// encrypted content we need to skip the data by reading it through the source, so as to ensure
// correct decryption of the remainder of the chunk. For clear content, we can request the
// remainder of the chunk directly.
DataSpec loadDataSpec;
boolean skipLoadedBytes;
if (isEncrypted) {
loadDataSpec = dataSpec;
skipLoadedBytes = bytesLoaded != 0;
private Extractor buildPackedAudioExtractor(long startTimeUs) {
Extractor extractor;
if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
extractor = new AdtsExtractor(startTimeUs);
} else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
extractor = new Ac3Extractor(startTimeUs);
} else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
extractor = new Mp3Extractor(startTimeUs);
} else {
loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
skipLoadedBytes = false;
throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment);
}
try {
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (skipLoadedBytes) {
input.skipFully(bytesLoaded);
}
try {
int result = Extractor.RESULT_CONTINUE;
if (!isMasterTimestampSource && timestampAdjuster != null) {
timestampAdjuster.waitUntilInitialized();
}
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
long adjustedEndTimeUs = extractorOutput.getLargestQueuedTimestampUs();
if (adjustedEndTimeUs != Long.MIN_VALUE) {
this.adjustedEndTimeUs = adjustedEndTimeUs;
}
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
/**
* If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in
* order to decrypt the loaded data. Else returns the original.
*/
private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey,
byte[] encryptionIv) {
if (encryptionKey == null || encryptionIv == null) {
return dataSource;
}
return new Aes128DataSource(dataSource, encryptionKey, encryptionIv);
extractor.init(extractorOutput);
return extractor;
}
}