Implement ID3 Metadata support for audio only HLS.

Issue: #862
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=111403855
This commit is contained in:
eguven 2016-01-05 04:42:59 -08:00 committed by Oliver Woodman
parent 1e4f2f6a1f
commit 69a42b60f8
7 changed files with 383 additions and 51 deletions

View file

@ -0,0 +1,175 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.testutil.FakeTrackOutput;
import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import junit.framework.TestCase;
import java.util.Arrays;
/**
* Test for {@link AdtsReader}.
*/
public class AdtsReaderTest extends TestCase {
public static final byte[] ID3_DATA_1 = TestUtil.createByteArray(
0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x54, 0x58,
0x58, 0x58, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x03, 0x00, 0x20, 0x2a,
0x2a, 0x2a, 0x20, 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x54,
0x69, 0x6d, 0x65, 0x64, 0x20, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74,
0x61, 0x20, 0x40, 0x20, 0x2d, 0x2d, 0x20, 0x30, 0x30, 0x3a, 0x30, 0x30,
0x3a, 0x30, 0x30, 0x2e, 0x30, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x00);
public static final byte[] ID3_DATA_2 = TestUtil.createByteArray(
0x49,
0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x50, 0x52, 0x49,
0x56, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61,
0x70, 0x70, 0x6c, 0x65, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69,
0x6e, 0x67, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xbb, 0xa0);
public static final byte[] ADTS_HEADER = TestUtil.createByteArray(
0xff, 0xf1, 0x50, 0x80, 0x01, 0xdf, 0xfc);
public static final byte[] ADTS_CONTENT = TestUtil.createByteArray(
0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e);
private static final byte TEST_DATA[] = TestUtil.joinByteArrays(
ID3_DATA_1,
ID3_DATA_2,
ADTS_HEADER,
ADTS_CONTENT);
private static final long ADTS_SAMPLE_DURATION = 23219L;
private ParsableByteArray data;
private AdtsReader adtsReader;
private FakeTrackOutput adtsOutput;
private FakeTrackOutput id3Output;
@Override
protected void setUp() throws Exception {
adtsOutput = new FakeTrackOutput();
id3Output = new FakeTrackOutput();
adtsReader = new AdtsReader(adtsOutput, id3Output);
data = new ParsableByteArray(TEST_DATA);
}
public void testSkipToNextSample() throws Exception {
for (int i = 1; i <= ID3_DATA_1.length + ID3_DATA_2.length; i++) {
data.setPosition(i);
feed();
// Once the data position set to ID3_DATA_1.length, no more id3 samples are read
int id3SampleCount = Math.min(i, ID3_DATA_1.length);
assertSampleCounts(id3SampleCount, i);
}
}
public void testSkipToNextSampleResetsState() throws Exception {
data = new ParsableByteArray(TestUtil.joinByteArrays(
ADTS_HEADER,
ADTS_CONTENT,
// Adts sample missing the first sync byte
Arrays.copyOfRange(ADTS_HEADER, 1, ADTS_HEADER.length),
ADTS_CONTENT));
feed();
assertSampleCounts(0, 1);
adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.SAMPLE_FLAG_SYNC, null);
}
public void testNoData() throws Exception {
feedLimited(0);
assertSampleCounts(0, 0);
}
public void testNotEnoughDataForIdentifier() throws Exception {
feedLimited(3 - 1);
assertSampleCounts(0, 0);
}
public void testNotEnoughDataForHeader() throws Exception {
feedLimited(10 - 1);
assertSampleCounts(0, 0);
}
public void testNotEnoughDataForWholeId3Packet() throws Exception {
feedLimited(ID3_DATA_1.length - 1);
assertSampleCounts(0, 0);
}
public void testConsumeWholeId3Packet() throws Exception {
feedLimited(ID3_DATA_1.length);
assertSampleCounts(1, 0);
id3Output.assertSample(0, ID3_DATA_1, 0, C.SAMPLE_FLAG_SYNC, null);
}
public void testMultiId3Packet() throws Exception {
feedLimited(ID3_DATA_1.length + ID3_DATA_2.length - 1);
assertSampleCounts(1, 0);
id3Output.assertSample(0, ID3_DATA_1, 0, C.SAMPLE_FLAG_SYNC, null);
}
public void testMultiId3PacketConsumed() throws Exception {
feedLimited(ID3_DATA_1.length + ID3_DATA_2.length);
assertSampleCounts(2, 0);
id3Output.assertSample(0, ID3_DATA_1, 0, C.SAMPLE_FLAG_SYNC, null);
id3Output.assertSample(1, ID3_DATA_2, 0, C.SAMPLE_FLAG_SYNC, null);
}
public void testMultiPacketConsumed() throws Exception {
for (int i = 0; i < 10; i++) {
data.setPosition(0);
adtsReader.consume(data, 0, i == 0);
long timeUs = ADTS_SAMPLE_DURATION * i;
int j = i * 2;
assertSampleCounts(j + 2, i + 1);
id3Output.assertSample(j, ID3_DATA_1, timeUs, C.SAMPLE_FLAG_SYNC, null);
id3Output.assertSample(j + 1, ID3_DATA_2, timeUs, C.SAMPLE_FLAG_SYNC, null);
adtsOutput.assertSample(i, ADTS_CONTENT, timeUs, C.SAMPLE_FLAG_SYNC, null);
}
}
public void testAdtsDataOnly() throws Exception {
data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length);
feed();
assertSampleCounts(0, 1);
adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.SAMPLE_FLAG_SYNC, null);
}
private void feedLimited(int limit) {
data.setLimit(limit);
feed();
}
private void feed() {
adtsReader.consume(data, 0, true);
}
private void assertSampleCounts(int id3SampleCount, int adtsSampleCount) {
id3Output.assertSampleCount(id3SampleCount);
adtsOutput.assertSampleCount(adtsSampleCount);
}
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
@ -180,6 +181,11 @@ public final class MediaFormat {
NO_VALUE);
}
public static MediaFormat createId3Format() {
return createFormatForMimeType(null, MimeTypes.APPLICATION_ID3, MediaFormat.NO_VALUE,
C.UNKNOWN_TIME_US);
}
/* package */ MediaFormat(String trackId, String mimeType, int bitrate, int maxInputSize,
long durationUs, int width, int height, int rotationDegrees, float pixelWidthHeightRatio,
int channelCount, int sampleRate, String language, long subsampleOffsetUs,

View file

@ -0,0 +1,47 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* A dummy {@link TrackOutput} implementation.
*/
public class DummyTrackOutput implements TrackOutput {
@Override
public void format(MediaFormat format) {
// Do nothing.
}
@Override
public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
return input.skip(length);
}
@Override
public void sampleData(ParsableByteArray data, int length) {
data.skipBytes(length);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
// Do nothing.
}
}

View file

@ -110,7 +110,7 @@ public final class AdtsExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
adtsReader = new AdtsReader(output.track(0));
adtsReader = new AdtsReader(output.track(0), output.track(1));
output.endTracks();
output.seekMap(SeekMap.UNSEEKABLE);
}

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Pair;
import java.util.Arrays;
import java.util.Collections;
/**
@ -32,20 +33,34 @@ import java.util.Collections;
*/
/* package */ final class AdtsReader extends ElementaryStreamReader {
private static final int STATE_FINDING_SYNC = 0;
private static final int STATE_READING_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2;
private static final int STATE_FINDING_SAMPLE = 0;
private static final int STATE_READING_ID3_HEADER = 1;
private static final int STATE_READING_ADTS_HEADER = 2;
private static final int STATE_READING_SAMPLE = 3;
private static final int HEADER_SIZE = 5;
private static final int CRC_SIZE = 2;
// Match states used while looking for the next sample
private static final int MATCH_STATE_VALUE_SHIFT = 8;
private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT;
private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT;
private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT;
private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT;
private static final int ID3_HEADER_SIZE = 10;
private static final int ID3_SIZE_OFFSET = 6;
private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
private final ParsableBitArray adtsScratch;
private final ParsableByteArray id3HeaderBuffer;
private final TrackOutput id3Output;
private int state;
private int bytesRead;
// Used to find the header.
private boolean lastByteWasFF;
private int matchState;
private boolean hasCrc;
// Used when parsing the header.
@ -56,17 +71,25 @@ import java.util.Collections;
// Used when reading the samples.
private long timeUs;
public AdtsReader(TrackOutput output) {
private TrackOutput currentOutput;
private long currentSampleDuration;
/**
* @param output A {@link TrackOutput} to which AAC samples should be written.
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
*/
public AdtsReader(TrackOutput output, TrackOutput id3Output) {
super(output);
this.id3Output = id3Output;
id3Output.format(MediaFormat.createId3Format());
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
state = STATE_FINDING_SYNC;
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
setFindingSampleState();
}
@Override
public void seek() {
state = STATE_FINDING_SYNC;
bytesRead = 0;
lastByteWasFF = false;
setFindingSampleState();
}
@Override
@ -76,30 +99,22 @@ import java.util.Collections;
}
while (data.bytesLeft() > 0) {
switch (state) {
case STATE_FINDING_SYNC:
if (skipToNextSync(data)) {
bytesRead = 0;
state = STATE_READING_HEADER;
case STATE_FINDING_SAMPLE:
findNextSample(data);
break;
case STATE_READING_ID3_HEADER:
if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) {
parseId3Header();
}
break;
case STATE_READING_HEADER:
case STATE_READING_ADTS_HEADER:
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.data, targetLength)) {
parseHeader();
bytesRead = 0;
state = STATE_READING_SAMPLE;
parseAdtsHeader();
}
break;
case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
output.sampleData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
timeUs += sampleDurationUs;
bytesRead = 0;
state = STATE_FINDING_SYNC;
}
readSample(data);
break;
}
}
@ -127,36 +142,109 @@ import java.util.Collections;
}
/**
* Locates the next sync word, advancing the position to the byte that immediately follows it.
* If a sync word was not located, the position is advanced to the limit.
* Sets the state to STATE_FINDING_SAMPLE.
*/
private void setFindingSampleState() {
state = STATE_FINDING_SAMPLE;
bytesRead = 0;
matchState = MATCH_STATE_START;
}
/**
* Sets the state to STATE_READING_ID3_HEADER and resets the fields required for
* {@link #parseId3Header()}.
*/
private void setReadingId3HeaderState() {
state = STATE_READING_ID3_HEADER;
bytesRead = ID3_IDENTIFIER.length;
sampleSize = 0;
id3HeaderBuffer.setPosition(0);
}
/**
* Sets the state to STATE_READING_SAMPLE.
*
* @param outputToUse TrackOutput object to write the sample to
* @param currentSampleDuration Duration of the sample to be read
* @param priorReadBytes Size of prior read bytes
* @param sampleSize Size of the sample
*/
private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration,
int priorReadBytes, int sampleSize) {
state = STATE_READING_SAMPLE;
bytesRead = priorReadBytes;
this.currentOutput = outputToUse;
this.currentSampleDuration = currentSampleDuration;
this.sampleSize = sampleSize;
}
/**
* Sets the state to STATE_READING_ADTS_HEADER.
*/
private void setReadingAdtsHeaderState() {
state = STATE_READING_ADTS_HEADER;
bytesRead = 0;
}
/**
* Locates the next sample start, advancing the position to the byte that immediately follows
* identifier. If a sample was not located, the position is advanced to the limit.
*
* @param pesBuffer The buffer whose position should be advanced.
* @return True if a sync word position was found. False otherwise.
*/
private boolean skipToNextSync(ParsableByteArray pesBuffer) {
private void findNextSample(ParsableByteArray pesBuffer) {
byte[] adtsData = pesBuffer.data;
int startOffset = pesBuffer.getPosition();
int position = pesBuffer.getPosition();
int endOffset = pesBuffer.limit();
for (int i = startOffset; i < endOffset; i++) {
boolean byteIsFF = (adtsData[i] & 0xFF) == 0xFF;
boolean found = lastByteWasFF && !byteIsFF && (adtsData[i] & 0xF0) == 0xF0;
lastByteWasFF = byteIsFF;
if (found) {
hasCrc = (adtsData[i] & 0x1) == 0;
pesBuffer.setPosition(i + 1);
// Reset lastByteWasFF for next time.
lastByteWasFF = false;
return true;
while (position < endOffset) {
int data = adtsData[position++] & 0xFF;
if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) {
hasCrc = (data & 0x1) == 0;
setReadingAdtsHeaderState();
pesBuffer.setPosition(position);
return;
}
switch (matchState | data) {
case MATCH_STATE_START | 0xFF:
matchState = MATCH_STATE_FF;
break;
case MATCH_STATE_START | 'I':
matchState = MATCH_STATE_I;
break;
case MATCH_STATE_I | 'D':
matchState = MATCH_STATE_ID;
break;
case MATCH_STATE_ID | '3':
setReadingId3HeaderState();
pesBuffer.setPosition(position);
return;
default:
if (matchState != MATCH_STATE_START) {
// If matching fails in a later state, revert to MATCH_STATE_START and
// check this byte again
matchState = MATCH_STATE_START;
position--;
}
break;
}
}
pesBuffer.setPosition(endOffset);
return false;
pesBuffer.setPosition(position);
}
/**
* Parses the Id3 header.
*/
private void parseId3Header() {
id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);
id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);
setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE,
id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);
}
/**
* Parses the sample header.
*/
private void parseHeader() {
private void parseAdtsHeader() {
adtsScratch.setPosition(0);
if (!hasOutputFormat) {
@ -183,10 +271,26 @@ import java.util.Collections;
}
adtsScratch.skipBits(4);
sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
if (hasCrc) {
sampleSize -= CRC_SIZE;
}
setReadingSampleState(output, sampleDurationUs, 0, sampleSize);
}
/**
* Reads the rest of the sample
*/
private void readSample(ParsableByteArray data) {
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
currentOutput.sampleData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
currentOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
timeUs += currentSampleDuration;
setFindingSampleState();
}
}
}

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
@ -35,8 +34,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
public Id3Reader(TrackOutput output) {
super(output);
output.format(MediaFormat.createFormatForMimeType(null, MimeTypes.APPLICATION_ID3,
MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US));
output.format(MediaFormat.createId3Format());
}
@Override

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.extractor.DummyTrackOutput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
@ -334,7 +335,8 @@ public final class TsExtractor implements Extractor {
pesPayloadReader = new MpegAudioReader(output.track(TS_STREAM_TYPE_MPA_LSF));
break;
case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC),
new DummyTrackOutput());
break;
case TS_STREAM_TYPE_AC3:
pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_AC3), false);