Commit 69a42b60 by eguven Committed by Oliver Woodman

Implement ID3 Metadata support for audio only HLS.

Issue: #862
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=111403855
parent 1e4f2f6a
/*
* 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);
}
}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
...@@ -180,6 +181,11 @@ public final class MediaFormat { ...@@ -180,6 +181,11 @@ public final class MediaFormat {
NO_VALUE); 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, /* package */ MediaFormat(String trackId, String mimeType, int bitrate, int maxInputSize,
long durationUs, int width, int height, int rotationDegrees, float pixelWidthHeightRatio, long durationUs, int width, int height, int rotationDegrees, float pixelWidthHeightRatio,
int channelCount, int sampleRate, String language, long subsampleOffsetUs, int channelCount, int sampleRate, String language, long subsampleOffsetUs,
......
/*
* 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.
}
}
...@@ -110,7 +110,7 @@ public final class AdtsExtractor implements Extractor { ...@@ -110,7 +110,7 @@ public final class AdtsExtractor implements Extractor {
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
adtsReader = new AdtsReader(output.track(0)); adtsReader = new AdtsReader(output.track(0), output.track(1));
output.endTracks(); output.endTracks();
output.seekMap(SeekMap.UNSEEKABLE); output.seekMap(SeekMap.UNSEEKABLE);
} }
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Pair; import android.util.Pair;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
/** /**
...@@ -32,20 +33,34 @@ import java.util.Collections; ...@@ -32,20 +33,34 @@ import java.util.Collections;
*/ */
/* package */ final class AdtsReader extends ElementaryStreamReader { /* package */ final class AdtsReader extends ElementaryStreamReader {
private static final int STATE_FINDING_SYNC = 0; private static final int STATE_FINDING_SAMPLE = 0;
private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_ID3_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2; 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 HEADER_SIZE = 5;
private static final int CRC_SIZE = 2; 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 ParsableBitArray adtsScratch;
private final ParsableByteArray id3HeaderBuffer;
private final TrackOutput id3Output;
private int state; private int state;
private int bytesRead; private int bytesRead;
// Used to find the header. private int matchState;
private boolean lastByteWasFF;
private boolean hasCrc; private boolean hasCrc;
// Used when parsing the header. // Used when parsing the header.
...@@ -56,17 +71,25 @@ import java.util.Collections; ...@@ -56,17 +71,25 @@ import java.util.Collections;
// Used when reading the samples. // Used when reading the samples.
private long timeUs; 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); super(output);
this.id3Output = id3Output;
id3Output.format(MediaFormat.createId3Format());
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); 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 @Override
public void seek() { public void seek() {
state = STATE_FINDING_SYNC; setFindingSampleState();
bytesRead = 0;
lastByteWasFF = false;
} }
@Override @Override
...@@ -76,30 +99,22 @@ import java.util.Collections; ...@@ -76,30 +99,22 @@ import java.util.Collections;
} }
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SYNC: case STATE_FINDING_SAMPLE:
if (skipToNextSync(data)) { findNextSample(data);
bytesRead = 0; break;
state = STATE_READING_HEADER; case STATE_READING_ID3_HEADER:
if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) {
parseId3Header();
} }
break; break;
case STATE_READING_HEADER: case STATE_READING_ADTS_HEADER:
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.data, targetLength)) { if (continueRead(data, adtsScratch.data, targetLength)) {
parseHeader(); parseAdtsHeader();
bytesRead = 0;
state = STATE_READING_SAMPLE;
} }
break; break;
case STATE_READING_SAMPLE: case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); readSample(data);
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;
}
break; break;
} }
} }
...@@ -127,36 +142,109 @@ import java.util.Collections; ...@@ -127,36 +142,109 @@ import java.util.Collections;
} }
/** /**
* Locates the next sync word, advancing the position to the byte that immediately follows it. * Sets the state to STATE_FINDING_SAMPLE.
* If a sync word was not located, the position is advanced to the limit. */
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. * @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; byte[] adtsData = pesBuffer.data;
int startOffset = pesBuffer.getPosition(); int position = pesBuffer.getPosition();
int endOffset = pesBuffer.limit(); int endOffset = pesBuffer.limit();
for (int i = startOffset; i < endOffset; i++) { while (position < endOffset) {
boolean byteIsFF = (adtsData[i] & 0xFF) == 0xFF; int data = adtsData[position++] & 0xFF;
boolean found = lastByteWasFF && !byteIsFF && (adtsData[i] & 0xF0) == 0xF0; if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) {
lastByteWasFF = byteIsFF; hasCrc = (data & 0x1) == 0;
if (found) { setReadingAdtsHeaderState();
hasCrc = (adtsData[i] & 0x1) == 0; pesBuffer.setPosition(position);
pesBuffer.setPosition(i + 1); return;
// Reset lastByteWasFF for next time. }
lastByteWasFF = false; switch (matchState | data) {
return true; 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); pesBuffer.setPosition(position);
return false; }
/**
* 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. * Parses the sample header.
*/ */
private void parseHeader() { private void parseAdtsHeader() {
adtsScratch.setPosition(0); adtsScratch.setPosition(0);
if (!hasOutputFormat) { if (!hasOutputFormat) {
...@@ -183,10 +271,26 @@ import java.util.Collections; ...@@ -183,10 +271,26 @@ import java.util.Collections;
} }
adtsScratch.skipBits(4); 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) { if (hasCrc) {
sampleSize -= CRC_SIZE; 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();
}
} }
} }
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.ts; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -35,8 +34,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -35,8 +34,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
public Id3Reader(TrackOutput output) { public Id3Reader(TrackOutput output) {
super(output); super(output);
output.format(MediaFormat.createFormatForMimeType(null, MimeTypes.APPLICATION_ID3, output.format(MediaFormat.createId3Format());
MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US));
} }
@Override @Override
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.extractor.ts; 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.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
...@@ -334,7 +335,8 @@ public final class TsExtractor implements Extractor { ...@@ -334,7 +335,8 @@ public final class TsExtractor implements Extractor {
pesPayloadReader = new MpegAudioReader(output.track(TS_STREAM_TYPE_MPA_LSF)); pesPayloadReader = new MpegAudioReader(output.track(TS_STREAM_TYPE_MPA_LSF));
break; break;
case TS_STREAM_TYPE_AAC: 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; break;
case TS_STREAM_TYPE_AC3: case TS_STREAM_TYPE_AC3:
pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_AC3), false); pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_AC3), false);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment