mirror of
https://github.com/samsonjs/media.git
synced 2026-04-17 13:15:47 +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.
|
||||
assertTrue(Arrays.equals(TEST_DATA, target));
|
||||
assertEquals(0, input.getPosition());
|
||||
assertEquals(TEST_DATA.length, input.getPeekPosition());
|
||||
|
||||
// Check that we can read again from the buffer
|
||||
byte[] target2 = new byte[TEST_DATA.length];
|
||||
input.readFully(target2, 0, TEST_DATA.length);
|
||||
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
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public final class OggReaderTest extends TestCase {
|
|||
assertEquals(0x02, oggReader.getPageHeader().type);
|
||||
assertEquals(27 + 1, oggReader.getPageHeader().headerSize);
|
||||
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(1000, oggReader.getPageHeader().pageSequenceNumber);
|
||||
assertEquals(4096, oggReader.getPageHeader().streamSerialNumber);
|
||||
|
|
|
|||
|
|
@ -25,12 +25,11 @@ import java.io.IOException;
|
|||
* Implementation of {@link ExtractorInput} for testing purpose.
|
||||
*/
|
||||
/* package */ class RecordableExtractorInput implements ExtractorInput {
|
||||
protected static final byte STREAM_REVISION = 0x00;
|
||||
|
||||
private byte[] data;
|
||||
private int readOffset;
|
||||
private int writeOffset;
|
||||
private int peekOffset;
|
||||
private int readPosition;
|
||||
private int writePosition;
|
||||
private int peekPosition;
|
||||
|
||||
private boolean throwExceptionsAtRead = false;
|
||||
private boolean throwExceptionsAtPeek = false;
|
||||
|
|
@ -47,12 +46,12 @@ import java.io.IOException;
|
|||
/**
|
||||
* Constructs an instance with a initial array of bytes.
|
||||
*
|
||||
* @param data the initial data.
|
||||
* @param writeOffset the {@code writeOffset} from where to start recording.
|
||||
* @param data The initial data.
|
||||
* @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.writeOffset = writeOffset;
|
||||
this.writePosition = writePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,15 +81,15 @@ import java.io.IOException;
|
|||
readExceptionCounter++;
|
||||
throw new IOException("deliberately thrown an exception for testing");
|
||||
}
|
||||
if (readOffset + length > writeOffset) {
|
||||
if (readPosition + length > writePosition) {
|
||||
if (!allowEndOfInput) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
System.arraycopy(data, readOffset, target, offset, length);
|
||||
readOffset += length;
|
||||
peekOffset = readOffset;
|
||||
System.arraycopy(data, readPosition, target, offset, length);
|
||||
readPosition += length;
|
||||
peekPosition = readPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -107,20 +106,20 @@ import java.io.IOException;
|
|||
}
|
||||
|
||||
private boolean isEOF() {
|
||||
return readOffset == writeOffset;
|
||||
return readPosition == writePosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean skipFully(int length, boolean allowEndOfInput)
|
||||
throws IOException, InterruptedException {
|
||||
if (readOffset + length >= writeOffset) {
|
||||
if (readPosition + length >= writePosition) {
|
||||
if (!allowEndOfInput) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
readOffset += length;
|
||||
peekOffset = readOffset;
|
||||
readPosition += length;
|
||||
peekPosition = readPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -141,14 +140,14 @@ import java.io.IOException;
|
|||
peekExceptionCounter++;
|
||||
throw new IOException("deliberately thrown an exception for testing");
|
||||
}
|
||||
if (peekOffset + length > writeOffset) {
|
||||
if (peekPosition + length > writePosition) {
|
||||
if (!allowEndOfInput) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
System.arraycopy(data, peekOffset, target, offset, length);
|
||||
peekOffset += length;
|
||||
System.arraycopy(data, peekPosition, target, offset, length);
|
||||
peekPosition += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -161,13 +160,13 @@ import java.io.IOException;
|
|||
@Override
|
||||
public boolean advancePeekPosition(int length, boolean allowEndOfInput)
|
||||
throws IOException, InterruptedException {
|
||||
if (peekOffset + length >= writeOffset) {
|
||||
if (peekPosition + length >= writePosition) {
|
||||
if (!allowEndOfInput) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
peekOffset += length;
|
||||
peekPosition += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -178,17 +177,22 @@ import java.io.IOException;
|
|||
|
||||
@Override
|
||||
public void resetPeekPosition() {
|
||||
peekOffset = readOffset;
|
||||
peekPosition = readPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPeekPosition() {
|
||||
return peekPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition() {
|
||||
return readOffset;
|
||||
return readPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLength() {
|
||||
return writeOffset;
|
||||
return writePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -197,13 +201,13 @@ import java.io.IOException;
|
|||
* @param bytes the bytes to record.
|
||||
*/
|
||||
public void record(final byte[] bytes) {
|
||||
System.arraycopy(bytes, 0, data, writeOffset, bytes.length);
|
||||
writeOffset += bytes.length;
|
||||
System.arraycopy(bytes, 0, data, writePosition, bytes.length);
|
||||
writePosition += bytes.length;
|
||||
}
|
||||
|
||||
/** Records a single byte. **/
|
||||
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 {
|
||||
|
||||
public static final byte STREAM_REVISION = 0x00;
|
||||
|
||||
private long pageSequenceCounter;
|
||||
|
||||
public RecordableOggExtractorInput(byte[] data, int writeOffset) {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,11 @@ public final class DefaultExtractorInput implements ExtractorInput {
|
|||
peekBufferPosition = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPeekPosition() {
|
||||
return position + peekBufferPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition() {
|
||||
return position;
|
||||
|
|
|
|||
|
|
@ -203,7 +203,14 @@ public interface ExtractorInput {
|
|||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
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.Util;
|
||||
|
||||
|
|
@ -23,8 +24,6 @@ import android.util.Pair;
|
|||
|
||||
import java.io.IOException;
|
||||
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.
|
||||
|
|
@ -37,9 +36,6 @@ import java.util.regex.Pattern;
|
|||
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
|
||||
|
||||
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"),
|
||||
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.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which data should be peeked.
|
||||
* @param out {@link Mp3Extractor.Metadata} to populate based on the input.
|
||||
* @return The number of bytes peeked from the input.
|
||||
* @return The gapless playback information, if present and non-zero. {@code null} otherwise.
|
||||
* @throws IOException If an error occurred peeking from the input.
|
||||
* @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 {
|
||||
out.encoderDelay = 0;
|
||||
out.encoderPadding = 0;
|
||||
ParsableByteArray scratch = new ParsableByteArray(10);
|
||||
int peekedId3Bytes = 0;
|
||||
GaplessInfo metadata = null;
|
||||
while (true) {
|
||||
input.peekFully(scratch.data, 0, 10);
|
||||
scratch.setPosition(0);
|
||||
|
|
@ -69,10 +63,10 @@ import java.util.regex.Pattern;
|
|||
int minorVersion = scratch.readUnsignedByte();
|
||||
int flags = scratch.readUnsignedByte();
|
||||
int length = scratch.readSynchSafeInt();
|
||||
if (canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
||||
if (metadata == null && canParseMetadata(majorVersion, minorVersion, flags, length)) {
|
||||
byte[] frame = new byte[length];
|
||||
input.peekFully(frame, 0, length);
|
||||
parseMetadata(new ParsableByteArray(frame), majorVersion, flags, out);
|
||||
metadata = parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags);
|
||||
} else {
|
||||
input.advancePeekPosition(length);
|
||||
}
|
||||
|
|
@ -81,7 +75,7 @@ import java.util.regex.Pattern;
|
|||
}
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(peekedId3Bytes);
|
||||
return peekedId3Bytes;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
|
||||
|
|
@ -93,19 +87,18 @@ import java.util.regex.Pattern;
|
|||
&& !(majorVersion == 4 && (flags & 0x0F) != 0);
|
||||
}
|
||||
|
||||
private static void parseMetadata(ParsableByteArray frame, int version, int flags,
|
||||
Mp3Extractor.Metadata out) {
|
||||
private static GaplessInfo parseGaplessInfo(ParsableByteArray frame, int version, int flags) {
|
||||
unescape(frame, version, flags);
|
||||
|
||||
// Skip any extended header.
|
||||
frame.setPosition(0);
|
||||
if (version == 3 && (flags & 0x40) != 0) {
|
||||
if (frame.bytesLeft() < 4) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
int extendedHeaderSize = frame.readUnsignedIntToInt();
|
||||
if (extendedHeaderSize > frame.bytesLeft()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
int paddingSize = 0;
|
||||
if (extendedHeaderSize >= 6) {
|
||||
|
|
@ -114,17 +107,17 @@ import java.util.regex.Pattern;
|
|||
frame.setPosition(4);
|
||||
frame.setLimit(frame.limit() - paddingSize);
|
||||
if (frame.bytesLeft() < extendedHeaderSize) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
frame.skipBytes(extendedHeaderSize);
|
||||
} else if (version == 4 && (flags & 0x40) != 0) {
|
||||
if (frame.bytesLeft() < 4) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
int extendedHeaderSize = frame.readSynchSafeInt();
|
||||
if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
frame.setPosition(extendedHeaderSize);
|
||||
}
|
||||
|
|
@ -132,20 +125,15 @@ import java.util.regex.Pattern;
|
|||
// Extract gapless playback metadata stored in comments.
|
||||
Pair<String, String> comment;
|
||||
while ((comment = findNextComment(version, frame)) != null) {
|
||||
if (comment.first.length() > 3 && comment.first.substring(3).equals(GAPLESS_COMMENT_NAME)) {
|
||||
Matcher matcher = GAPLESS_COMMENT_VALUE_PATTERN.matcher(comment.second);
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
out.encoderDelay = Integer.parseInt(matcher.group(1), 16);
|
||||
out.encoderPadding = Integer.parseInt(matcher.group(2), 16);
|
||||
break;
|
||||
} catch (NumberFormatException e) {
|
||||
out.encoderDelay = 0;
|
||||
return;
|
||||
}
|
||||
if (comment.first.length() > 3) {
|
||||
GaplessInfo gaplessInfo =
|
||||
GaplessInfo.createFromComment(comment.first.substring(3), comment.second);
|
||||
if (gaplessInfo != null) {
|
||||
return gaplessInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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.ExtractorInput;
|
||||
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.SeekMap;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
|
|
@ -56,7 +57,6 @@ public final class Mp3Extractor implements Extractor {
|
|||
private final long forcedFirstSampleTimestampUs;
|
||||
private final ParsableByteArray scratch;
|
||||
private final MpegAudioHeader synchronizedHeader;
|
||||
private final Metadata metadata;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
|
|
@ -64,6 +64,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
|
||||
private int synchronizedHeaderData;
|
||||
|
||||
private GaplessInfo gaplessInfo;
|
||||
private Seeker seeker;
|
||||
private long basisTimeUs;
|
||||
private int samplesRead;
|
||||
|
|
@ -86,7 +87,6 @@ public final class Mp3Extractor implements Extractor {
|
|||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(4);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
metadata = new Metadata();
|
||||
basisTimeUs = -1;
|
||||
}
|
||||
|
||||
|
|
@ -194,11 +194,15 @@ public final class Mp3Extractor implements Extractor {
|
|||
|
||||
private boolean synchronize(ExtractorInput input, boolean sniffing)
|
||||
throws IOException, InterruptedException {
|
||||
input.resetPeekPosition();
|
||||
int searched = 0;
|
||||
int validFrameCount = 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) {
|
||||
if (sniffing && searched == MAX_SNIFF_BYTES) {
|
||||
return false;
|
||||
|
|
@ -274,15 +278,13 @@ public final class Mp3Extractor implements Extractor {
|
|||
int headerData = frame.readInt();
|
||||
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
||||
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.
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(xingBase + 141);
|
||||
input.peekFully(scratch.data, 0, 3);
|
||||
scratch.setPosition(0);
|
||||
int gaplessMetadata = scratch.readUnsignedInt24();
|
||||
metadata.encoderDelay = gaplessMetadata >> 12;
|
||||
metadata.encoderPadding = gaplessMetadata & 0x0FFF;
|
||||
gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24());
|
||||
}
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
} 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_samr = Util.getIntegerCodeForString("samr");
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.extractor.mp4;
|
|||
|
||||
import com.google.android.exoplayer.C;
|
||||
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.Assertions;
|
||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||
|
|
@ -330,6 +331,71 @@ import java.util.List;
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -275,11 +275,19 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
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) {
|
||||
long durationUs = C.UNKNOWN_TIME_US;
|
||||
List<Mp4Track> tracks = new ArrayList<>();
|
||||
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++) {
|
||||
Atom.ContainerAtom atom = moov.containerChildren.get(i);
|
||||
if (atom.type != Atom.TYPE_trak) {
|
||||
|
|
@ -421,19 +429,24 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
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) {
|
||||
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_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
|
||||
|| 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) {
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue