mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Extract gapless playback data in MP4 files.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=117148015
This commit is contained in:
parent
2380857bf2
commit
554817cca6
12 changed files with 258 additions and 80 deletions
|
|
@ -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 {
|
||||||
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;
|
||||||
* 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;
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
@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;
|
||||||
|
|
||||||
@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;
|
||||||
* @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;
|
||||||
*/
|
*/
|
||||||
/* 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) {
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,11 @@ public final class DefaultExtractorInput implements ExtractorInput {
|
||||||
peekBufferPosition = 0;
|
peekBufferPosition = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPeekPosition() {
|
||||||
|
return position + peekBufferPosition;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getPosition() {
|
public long getPosition() {
|
||||||
return position;
|
return position;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
* 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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
&& !(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;
|
||||||
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;
|
||||||
// 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;
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ static final class Metadata {
|
|
||||||
|
|
||||||
public int encoderDelay;
|
|
||||||
public int encoderPadding;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -330,6 +331,71 @@ import java.util.List;
|
||||||
editedFlags);
|
editedFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue