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:
parent
e5bc00ea94
commit
a66b4a9bad
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user