Commit 554817cc by olly Committed by Oliver Woodman

Extract gapless playback data in MP4 files.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117148015
parent 2380857b
...@@ -297,11 +297,14 @@ public class DefaultExtractorInputTest extends TestCase { ...@@ -297,11 +297,14 @@ public class DefaultExtractorInputTest extends TestCase {
// Check that we read the whole of TEST_DATA. // Check that we read the whole of TEST_DATA.
assertTrue(Arrays.equals(TEST_DATA, target)); assertTrue(Arrays.equals(TEST_DATA, target));
assertEquals(0, input.getPosition()); assertEquals(0, input.getPosition());
assertEquals(TEST_DATA.length, input.getPeekPosition());
// Check that we can read again from the buffer // Check that we can read again from the buffer
byte[] target2 = new byte[TEST_DATA.length]; byte[] target2 = new byte[TEST_DATA.length];
input.readFully(target2, 0, TEST_DATA.length); input.readFully(target2, 0, TEST_DATA.length);
assertTrue(Arrays.equals(TEST_DATA, target2)); assertTrue(Arrays.equals(TEST_DATA, target2));
assertEquals(TEST_DATA.length, input.getPosition());
assertEquals(TEST_DATA.length, input.getPeekPosition());
// Check that we fail with EOFException if we peek again // Check that we fail with EOFException if we peek again
try { try {
......
...@@ -73,7 +73,7 @@ public final class OggReaderTest extends TestCase { ...@@ -73,7 +73,7 @@ public final class OggReaderTest extends TestCase {
assertEquals(0x02, oggReader.getPageHeader().type); assertEquals(0x02, oggReader.getPageHeader().type);
assertEquals(27 + 1, oggReader.getPageHeader().headerSize); assertEquals(27 + 1, oggReader.getPageHeader().headerSize);
assertEquals(8, oggReader.getPageHeader().bodySize); assertEquals(8, oggReader.getPageHeader().bodySize);
assertEquals(RecordableExtractorInput.STREAM_REVISION, oggReader.getPageHeader().revision); assertEquals(RecordableOggExtractorInput.STREAM_REVISION, oggReader.getPageHeader().revision);
assertEquals(1, oggReader.getPageHeader().pageSegmentCount); assertEquals(1, oggReader.getPageHeader().pageSegmentCount);
assertEquals(1000, oggReader.getPageHeader().pageSequenceNumber); assertEquals(1000, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggReader.getPageHeader().streamSerialNumber); assertEquals(4096, oggReader.getPageHeader().streamSerialNumber);
......
...@@ -25,12 +25,11 @@ import java.io.IOException; ...@@ -25,12 +25,11 @@ import java.io.IOException;
* Implementation of {@link ExtractorInput} for testing purpose. * Implementation of {@link ExtractorInput} for testing purpose.
*/ */
/* package */ class RecordableExtractorInput implements ExtractorInput { /* package */ class RecordableExtractorInput implements ExtractorInput {
protected static final byte STREAM_REVISION = 0x00;
private byte[] data; private byte[] data;
private int readOffset; private int readPosition;
private int writeOffset; private int writePosition;
private int peekOffset; private int peekPosition;
private boolean throwExceptionsAtRead = false; private boolean throwExceptionsAtRead = false;
private boolean throwExceptionsAtPeek = false; private boolean throwExceptionsAtPeek = false;
...@@ -47,12 +46,12 @@ import java.io.IOException; ...@@ -47,12 +46,12 @@ import java.io.IOException;
/** /**
* Constructs an instance with a initial array of bytes. * Constructs an instance with a initial array of bytes.
* *
* @param data the initial data. * @param data The initial data.
* @param writeOffset the {@code writeOffset} from where to start recording. * @param writePosition The {@code writePosition} from where to start recording.
*/ */
public RecordableExtractorInput(byte[] data, int writeOffset) { public RecordableExtractorInput(byte[] data, int writePosition) {
this.data = data; this.data = data;
this.writeOffset = writeOffset; this.writePosition = writePosition;
} }
/** /**
...@@ -82,15 +81,15 @@ import java.io.IOException; ...@@ -82,15 +81,15 @@ import java.io.IOException;
readExceptionCounter++; readExceptionCounter++;
throw new IOException("deliberately thrown an exception for testing"); throw new IOException("deliberately thrown an exception for testing");
} }
if (readOffset + length > writeOffset) { if (readPosition + length > writePosition) {
if (!allowEndOfInput) { if (!allowEndOfInput) {
throw new EOFException(); throw new EOFException();
} }
return false; return false;
} }
System.arraycopy(data, readOffset, target, offset, length); System.arraycopy(data, readPosition, target, offset, length);
readOffset += length; readPosition += length;
peekOffset = readOffset; peekPosition = readPosition;
return true; return true;
} }
...@@ -107,20 +106,20 @@ import java.io.IOException; ...@@ -107,20 +106,20 @@ import java.io.IOException;
} }
private boolean isEOF() { private boolean isEOF() {
return readOffset == writeOffset; return readPosition == writePosition;
} }
@Override @Override
public boolean skipFully(int length, boolean allowEndOfInput) public boolean skipFully(int length, boolean allowEndOfInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (readOffset + length >= writeOffset) { if (readPosition + length >= writePosition) {
if (!allowEndOfInput) { if (!allowEndOfInput) {
throw new EOFException(); throw new EOFException();
} }
return false; return false;
} }
readOffset += length; readPosition += length;
peekOffset = readOffset; peekPosition = readPosition;
return true; return true;
} }
...@@ -141,14 +140,14 @@ import java.io.IOException; ...@@ -141,14 +140,14 @@ import java.io.IOException;
peekExceptionCounter++; peekExceptionCounter++;
throw new IOException("deliberately thrown an exception for testing"); throw new IOException("deliberately thrown an exception for testing");
} }
if (peekOffset + length > writeOffset) { if (peekPosition + length > writePosition) {
if (!allowEndOfInput) { if (!allowEndOfInput) {
throw new EOFException(); throw new EOFException();
} }
return false; return false;
} }
System.arraycopy(data, peekOffset, target, offset, length); System.arraycopy(data, peekPosition, target, offset, length);
peekOffset += length; peekPosition += length;
return true; return true;
} }
...@@ -161,13 +160,13 @@ import java.io.IOException; ...@@ -161,13 +160,13 @@ import java.io.IOException;
@Override @Override
public boolean advancePeekPosition(int length, boolean allowEndOfInput) public boolean advancePeekPosition(int length, boolean allowEndOfInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (peekOffset + length >= writeOffset) { if (peekPosition + length >= writePosition) {
if (!allowEndOfInput) { if (!allowEndOfInput) {
throw new EOFException(); throw new EOFException();
} }
return false; return false;
} }
peekOffset += length; peekPosition += length;
return true; return true;
} }
...@@ -178,17 +177,22 @@ import java.io.IOException; ...@@ -178,17 +177,22 @@ import java.io.IOException;
@Override @Override
public void resetPeekPosition() { public void resetPeekPosition() {
peekOffset = readOffset; peekPosition = readPosition;
}
@Override
public long getPeekPosition() {
return peekPosition;
} }
@Override @Override
public long getPosition() { public long getPosition() {
return readOffset; return readPosition;
} }
@Override @Override
public long getLength() { public long getLength() {
return writeOffset; return writePosition;
} }
/** /**
...@@ -197,13 +201,13 @@ import java.io.IOException; ...@@ -197,13 +201,13 @@ import java.io.IOException;
* @param bytes the bytes to record. * @param bytes the bytes to record.
*/ */
public void record(final byte[] bytes) { public void record(final byte[] bytes) {
System.arraycopy(bytes, 0, data, writeOffset, bytes.length); System.arraycopy(bytes, 0, data, writePosition, bytes.length);
writeOffset += bytes.length; writePosition += bytes.length;
} }
/** Records a single byte. **/ /** Records a single byte. **/
public void record(byte b) { public void record(byte b) {
record(new byte[]{b}); record(new byte[] {b});
} }
/** /**
......
...@@ -20,6 +20,8 @@ package com.google.android.exoplayer.extractor.ogg; ...@@ -20,6 +20,8 @@ package com.google.android.exoplayer.extractor.ogg;
*/ */
/* package */ final class RecordableOggExtractorInput extends RecordableExtractorInput { /* package */ final class RecordableOggExtractorInput extends RecordableExtractorInput {
public static final byte STREAM_REVISION = 0x00;
private long pageSequenceCounter; private long pageSequenceCounter;
public RecordableOggExtractorInput(byte[] data, int writeOffset) { public RecordableOggExtractorInput(byte[] data, int writeOffset) {
......
...@@ -148,6 +148,11 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -148,6 +148,11 @@ public final class DefaultExtractorInput implements ExtractorInput {
} }
@Override @Override
public long getPeekPosition() {
return position + peekBufferPosition;
}
@Override
public long getPosition() { public long getPosition() {
return position; return position;
} }
......
...@@ -203,7 +203,14 @@ public interface ExtractorInput { ...@@ -203,7 +203,14 @@ public interface ExtractorInput {
void resetPeekPosition(); void resetPeekPosition();
/** /**
* The current read position (byte offset) in the stream. * Returns the current peek position (byte offset) in the stream.
*
* @return The peek position (byte offset) in the stream.
*/
long getPeekPosition();
/**
* Returns the current read position (byte offset) in the stream.
* *
* @return The read position (byte offset) in the stream. * @return The read position (byte offset) in the stream.
*/ */
......
/*
* Copyright (C) 2014 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 java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility for parsing and representing gapless playback information.
*/
public final class GaplessInfo {
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
/**
* Parses a gapless playback comment (stored in an ID3 header or MPEG 4 user data).
*
* @param name The comment's identifier.
* @param data The comment's payload data.
* @return Parsed gapless playback information, if present and non-zero. {@code null} otherwise.
*/
public static GaplessInfo createFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return null;
}
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) {
try {
int encoderDelay = Integer.parseInt(matcher.group(1), 16);
int encoderPadding = Integer.parseInt(matcher.group(2), 16);
return encoderDelay == 0 && encoderPadding == 0 ? null
: new GaplessInfo(encoderDelay, encoderPadding);
} catch (NumberFormatException e) {
// Ignore incorrectly formatted comments.
}
}
return null;
}
/**
* Parses gapless playback information associated with an MP3 Xing header.
*
* @param value The 24-bit value to parse.
* @return Parsed gapless playback information, if non-zero. {@code null} otherwise.
*/
public static GaplessInfo createFromXingHeaderValue(int value) {
int encoderDelay = value >> 12;
int encoderPadding = value & 0x0FFF;
return encoderDelay == 0 && encoderPadding == 0 ? null
: new GaplessInfo(encoderDelay, encoderPadding);
}
/**
* The number of samples to trim from the start of the decoded audio stream.
*/
public final int encoderDelay;
/**
* The number of samples to trim from the end of the decoded audio stream.
*/
public final int encoderPadding;
/**
* Creates a new {@link GaplessInfo} with the specified encoder delay and padding.
*
* @param encoderDelay The encoder delay.
* @param encoderPadding The encoder padding.
*/
private GaplessInfo(int encoderDelay, int encoderPadding) {
this.encoderDelay = encoderDelay;
this.encoderPadding = encoderPadding;
}
}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.extractor.mp3; package com.google.android.exoplayer.extractor.mp3;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.GaplessInfo;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -23,8 +24,6 @@ import android.util.Pair; ...@@ -23,8 +24,6 @@ import android.util.Pair;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Utility for parsing ID3 version 2 metadata in MP3 files. * Utility for parsing ID3 version 2 metadata in MP3 files.
...@@ -37,9 +36,6 @@ import java.util.regex.Pattern; ...@@ -37,9 +36,6 @@ import java.util.regex.Pattern;
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024; private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
private static final String GAPLESS_COMMENT_NAME = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_VALUE_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
private static final Charset[] CHARSET_BY_ENCODING = new Charset[] {Charset.forName("ISO-8859-1"), private static final Charset[] CHARSET_BY_ENCODING = new Charset[] {Charset.forName("ISO-8859-1"),
Charset.forName("UTF-16LE"), Charset.forName("UTF-16BE"), Charset.forName("UTF-8")}; Charset.forName("UTF-16LE"), Charset.forName("UTF-16BE"), Charset.forName("UTF-8")};
...@@ -47,17 +43,15 @@ import java.util.regex.Pattern; ...@@ -47,17 +43,15 @@ import java.util.regex.Pattern;
* Peeks data from the input and parses ID3 metadata. * Peeks data from the input and parses ID3 metadata.
* *
* @param input The {@link ExtractorInput} from which data should be peeked. * @param input The {@link ExtractorInput} from which data should be peeked.
* @param out {@link Mp3Extractor.Metadata} to populate based on the input. * @return The gapless playback information, if present and non-zero. {@code null} otherwise.
* @return The number of bytes peeked from the input.
* @throws IOException If an error occurred peeking from the input. * @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
public static int parseId3(ExtractorInput input, Mp3Extractor.Metadata out) public static GaplessInfo parseId3(ExtractorInput input)
throws IOException, InterruptedException { throws IOException, InterruptedException {
out.encoderDelay = 0;
out.encoderPadding = 0;
ParsableByteArray scratch = new ParsableByteArray(10); ParsableByteArray scratch = new ParsableByteArray(10);
int peekedId3Bytes = 0; int peekedId3Bytes = 0;
GaplessInfo metadata = null;
while (true) { while (true) {
input.peekFully(scratch.data, 0, 10); input.peekFully(scratch.data, 0, 10);
scratch.setPosition(0); scratch.setPosition(0);
...@@ -69,10 +63,10 @@ import java.util.regex.Pattern; ...@@ -69,10 +63,10 @@ import java.util.regex.Pattern;
int minorVersion = scratch.readUnsignedByte(); int minorVersion = scratch.readUnsignedByte();
int flags = scratch.readUnsignedByte(); int flags = scratch.readUnsignedByte();
int length = scratch.readSynchSafeInt(); int length = scratch.readSynchSafeInt();
if (canParseMetadata(majorVersion, minorVersion, flags, length)) { if (metadata == null && canParseMetadata(majorVersion, minorVersion, flags, length)) {
byte[] frame = new byte[length]; byte[] frame = new byte[length];
input.peekFully(frame, 0, length); input.peekFully(frame, 0, length);
parseMetadata(new ParsableByteArray(frame), majorVersion, flags, out); metadata = parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags);
} else { } else {
input.advancePeekPosition(length); input.advancePeekPosition(length);
} }
...@@ -81,7 +75,7 @@ import java.util.regex.Pattern; ...@@ -81,7 +75,7 @@ import java.util.regex.Pattern;
} }
input.resetPeekPosition(); input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes); input.advancePeekPosition(peekedId3Bytes);
return peekedId3Bytes; return metadata;
} }
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
...@@ -93,19 +87,18 @@ import java.util.regex.Pattern; ...@@ -93,19 +87,18 @@ import java.util.regex.Pattern;
&& !(majorVersion == 4 && (flags & 0x0F) != 0); && !(majorVersion == 4 && (flags & 0x0F) != 0);
} }
private static void parseMetadata(ParsableByteArray frame, int version, int flags, private static GaplessInfo parseGaplessInfo(ParsableByteArray frame, int version, int flags) {
Mp3Extractor.Metadata out) {
unescape(frame, version, flags); unescape(frame, version, flags);
// Skip any extended header. // Skip any extended header.
frame.setPosition(0); frame.setPosition(0);
if (version == 3 && (flags & 0x40) != 0) { if (version == 3 && (flags & 0x40) != 0) {
if (frame.bytesLeft() < 4) { if (frame.bytesLeft() < 4) {
return; return null;
} }
int extendedHeaderSize = frame.readUnsignedIntToInt(); int extendedHeaderSize = frame.readUnsignedIntToInt();
if (extendedHeaderSize > frame.bytesLeft()) { if (extendedHeaderSize > frame.bytesLeft()) {
return; return null;
} }
int paddingSize = 0; int paddingSize = 0;
if (extendedHeaderSize >= 6) { if (extendedHeaderSize >= 6) {
...@@ -114,17 +107,17 @@ import java.util.regex.Pattern; ...@@ -114,17 +107,17 @@ import java.util.regex.Pattern;
frame.setPosition(4); frame.setPosition(4);
frame.setLimit(frame.limit() - paddingSize); frame.setLimit(frame.limit() - paddingSize);
if (frame.bytesLeft() < extendedHeaderSize) { if (frame.bytesLeft() < extendedHeaderSize) {
return; return null;
} }
} }
frame.skipBytes(extendedHeaderSize); frame.skipBytes(extendedHeaderSize);
} else if (version == 4 && (flags & 0x40) != 0) { } else if (version == 4 && (flags & 0x40) != 0) {
if (frame.bytesLeft() < 4) { if (frame.bytesLeft() < 4) {
return; return null;
} }
int extendedHeaderSize = frame.readSynchSafeInt(); int extendedHeaderSize = frame.readSynchSafeInt();
if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) { if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) {
return; return null;
} }
frame.setPosition(extendedHeaderSize); frame.setPosition(extendedHeaderSize);
} }
...@@ -132,20 +125,15 @@ import java.util.regex.Pattern; ...@@ -132,20 +125,15 @@ import java.util.regex.Pattern;
// Extract gapless playback metadata stored in comments. // Extract gapless playback metadata stored in comments.
Pair<String, String> comment; Pair<String, String> comment;
while ((comment = findNextComment(version, frame)) != null) { while ((comment = findNextComment(version, frame)) != null) {
if (comment.first.length() > 3 && comment.first.substring(3).equals(GAPLESS_COMMENT_NAME)) { if (comment.first.length() > 3) {
Matcher matcher = GAPLESS_COMMENT_VALUE_PATTERN.matcher(comment.second); GaplessInfo gaplessInfo =
if (matcher.find()) { GaplessInfo.createFromComment(comment.first.substring(3), comment.second);
try { if (gaplessInfo != null) {
out.encoderDelay = Integer.parseInt(matcher.group(1), 16); return gaplessInfo;
out.encoderPadding = Integer.parseInt(matcher.group(2), 16);
break;
} catch (NumberFormatException e) {
out.encoderDelay = 0;
return;
}
} }
} }
} }
return null;
} }
private static Pair<String, String> findNextComment(int majorVersion, ParsableByteArray data) { private static Pair<String, String> findNextComment(int majorVersion, ParsableByteArray data) {
......
...@@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException; ...@@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException;
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;
import com.google.android.exoplayer.extractor.GaplessInfo;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
...@@ -56,7 +57,6 @@ public final class Mp3Extractor implements Extractor { ...@@ -56,7 +57,6 @@ public final class Mp3Extractor implements Extractor {
private final long forcedFirstSampleTimestampUs; private final long forcedFirstSampleTimestampUs;
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader; private final MpegAudioHeader synchronizedHeader;
private final Metadata metadata;
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
...@@ -64,6 +64,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -64,6 +64,7 @@ public final class Mp3Extractor implements Extractor {
private int synchronizedHeaderData; private int synchronizedHeaderData;
private GaplessInfo gaplessInfo;
private Seeker seeker; private Seeker seeker;
private long basisTimeUs; private long basisTimeUs;
private int samplesRead; private int samplesRead;
...@@ -86,7 +87,6 @@ public final class Mp3Extractor implements Extractor { ...@@ -86,7 +87,6 @@ public final class Mp3Extractor implements Extractor {
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(4); scratch = new ParsableByteArray(4);
synchronizedHeader = new MpegAudioHeader(); synchronizedHeader = new MpegAudioHeader();
metadata = new Metadata();
basisTimeUs = -1; basisTimeUs = -1;
} }
...@@ -194,11 +194,15 @@ public final class Mp3Extractor implements Extractor { ...@@ -194,11 +194,15 @@ public final class Mp3Extractor implements Extractor {
private boolean synchronize(ExtractorInput input, boolean sniffing) private boolean synchronize(ExtractorInput input, boolean sniffing)
throws IOException, InterruptedException { throws IOException, InterruptedException {
input.resetPeekPosition();
int searched = 0; int searched = 0;
int validFrameCount = 0; int validFrameCount = 0;
int candidateSynchronizedHeaderData = 0; int candidateSynchronizedHeaderData = 0;
int peekedId3Bytes = input.getPosition() == 0 ? Id3Util.parseId3(input, metadata) : 0; int peekedId3Bytes = 0;
input.resetPeekPosition();
if (input.getPosition() == 0) {
gaplessInfo = Id3Util.parseId3(input);
peekedId3Bytes = (int) input.getPeekPosition();
}
while (true) { while (true) {
if (sniffing && searched == MAX_SNIFF_BYTES) { if (sniffing && searched == MAX_SNIFF_BYTES) {
return false; return false;
...@@ -274,15 +278,13 @@ public final class Mp3Extractor implements Extractor { ...@@ -274,15 +278,13 @@ public final class Mp3Extractor implements Extractor {
int headerData = frame.readInt(); int headerData = frame.readInt();
if (headerData == XING_HEADER || headerData == INFO_HEADER) { if (headerData == XING_HEADER || headerData == INFO_HEADER) {
seeker = XingSeeker.create(synchronizedHeader, frame, position, length); seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
if (seeker != null && metadata.encoderDelay == 0 && metadata.encoderPadding == 0) { if (seeker != null && gaplessInfo == null) {
// If there is a Xing header, read gapless playback metadata at a fixed offset. // If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition(); input.resetPeekPosition();
input.advancePeekPosition(xingBase + 141); input.advancePeekPosition(xingBase + 141);
input.peekFully(scratch.data, 0, 3); input.peekFully(scratch.data, 0, 3);
scratch.setPosition(0); scratch.setPosition(0);
int gaplessMetadata = scratch.readUnsignedInt24(); gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24());
metadata.encoderDelay = gaplessMetadata >> 12;
metadata.encoderPadding = gaplessMetadata & 0x0FFF;
} }
input.skipFully(synchronizedHeader.frameSize); input.skipFully(synchronizedHeader.frameSize);
} else { } else {
...@@ -322,11 +324,4 @@ public final class Mp3Extractor implements Extractor { ...@@ -322,11 +324,4 @@ public final class Mp3Extractor implements Extractor {
} }
/* package */ static final class Metadata {
public int encoderDelay;
public int encoderPadding;
}
} }
...@@ -114,6 +114,13 @@ import java.util.List; ...@@ -114,6 +114,13 @@ import java.util.List;
public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp");
public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); public static final int TYPE_samr = Util.getIntegerCodeForString("samr");
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst");
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name");
public static final int TYPE_data = Util.getIntegerCodeForString("data");
public static final int TYPE_DASHES = Util.getIntegerCodeForString("----");
public final int type; public final int type;
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.extractor.mp4; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format; import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.GaplessInfo;
import com.google.android.exoplayer.util.Ac3Util; import com.google.android.exoplayer.util.Ac3Util;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
...@@ -331,6 +332,71 @@ import java.util.List; ...@@ -331,6 +332,71 @@ import java.util.List;
} }
/** /**
* Parses a udta atom.
*
* @param udtaAtom The udta (user data) atom to parse.
* @return Gapless playback information stored in the user data, or {@code null} if not present.
*/
public static GaplessInfo parseUdta(Atom.ContainerAtom udtaAtom) {
Atom.LeafAtom metaAtom = udtaAtom.getLeafAtomOfType(Atom.TYPE_meta);
if (metaAtom == null) {
return null;
}
ParsableByteArray data = metaAtom.data;
data.setPosition(Atom.FULL_HEADER_SIZE);
ParsableByteArray ilst = new ParsableByteArray();
while (data.bytesLeft() > 0) {
int length = data.readInt() - Atom.HEADER_SIZE;
int type = data.readInt();
if (type == Atom.TYPE_ilst) {
ilst.reset(data.data, data.getPosition() + length);
ilst.setPosition(data.getPosition());
GaplessInfo gaplessInfo = parseIlst(ilst);
if (gaplessInfo != null) {
return gaplessInfo;
}
}
data.skipBytes(length);
}
return null;
}
private static GaplessInfo parseIlst(ParsableByteArray ilst) {
while (ilst.bytesLeft() > 0) {
int position = ilst.getPosition();
int endPosition = position + ilst.readInt();
int type = ilst.readInt();
if (type == Atom.TYPE_DASHES) {
String lastCommentMean = null;
String lastCommentName = null;
String lastCommentData = null;
while (ilst.getPosition() < endPosition) {
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
int key = ilst.readInt();
ilst.skipBytes(4);
if (key == Atom.TYPE_mean) {
lastCommentMean = ilst.readString(length);
} else if (key == Atom.TYPE_name) {
lastCommentName = ilst.readString(length);
} else if (key == Atom.TYPE_data) {
ilst.skipBytes(4);
lastCommentData = ilst.readString(length - 4);
} else {
ilst.skipBytes(length);
}
}
if (lastCommentName != null && lastCommentData != null
&& "com.apple.iTunes".equals(lastCommentMean)) {
return GaplessInfo.createFromComment(lastCommentName, lastCommentData);
}
} else {
ilst.setPosition(endPosition);
}
}
return null;
}
/**
* Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie. * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.
* *
* @param mvhd Contents of the mvhd atom to be parsed. * @param mvhd Contents of the mvhd atom to be parsed.
......
...@@ -275,11 +275,19 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -275,11 +275,19 @@ public final class Mp4Extractor implements Extractor, SeekMap {
return false; return false;
} }
/** Updates the stored track metadata to reflect the contents of the specified moov atom. */ /**
* Updates the stored track metadata to reflect the contents of the specified moov atom.
*/
private void processMoovAtom(ContainerAtom moov) { private void processMoovAtom(ContainerAtom moov) {
long durationUs = C.UNKNOWN_TIME_US; long durationUs = C.UNKNOWN_TIME_US;
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE; long earliestSampleOffset = Long.MAX_VALUE;
// TODO: Apply gapless information.
// GaplessInfo gaplessInfo = null;
// Atom.ContainerAtom udta = moov.getContainerAtomOfType(Atom.TYPE_udta);
// if (udta != null) {
// gaplessInfo = AtomParsers.parseUdta(udta);
// }
for (int i = 0; i < moov.containerChildren.size(); i++) { for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i); Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) { if (atom.type != Atom.TYPE_trak) {
...@@ -421,19 +429,24 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -421,19 +429,24 @@ public final class Mp4Extractor implements Extractor, SeekMap {
return earliestSampleTrackIndex; return earliestSampleTrackIndex;
} }
/** Returns whether the extractor should parse a leaf atom with type {@code atom}. */ /**
* Returns whether the extractor should parse a leaf atom with type {@code atom}.
*/
private static boolean shouldParseLeafAtom(int atom) { private static boolean shouldParseLeafAtom(int atom) {
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|| atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
|| atom == Atom.TYPE_stsz || atom == Atom.TYPE_stco || atom == Atom.TYPE_co64 || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stco || atom == Atom.TYPE_co64
|| atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp; || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp || atom == Atom.TYPE_meta;
} }
/** Returns whether the extractor should parse a container atom with type {@code atom}. */ /**
* Returns whether the extractor should parse a container atom with type {@code atom}.
*/
private static boolean shouldParseContainerAtom(int atom) { private static boolean shouldParseContainerAtom(int atom) {
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts; || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts
|| atom == Atom.TYPE_udta;
} }
private static final class Mp4Track { private static final class Mp4Track {
......
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