Commit 9b574878 by andrewlewis Committed by Oliver Woodman

Handle gapless audio metadata in elst.

Only edit lists that truncate the first/last sample are supported, which is
sufficient to handle AAC encoder delay/padding.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=121011278
parent 23cb9532
...@@ -15,74 +15,91 @@ ...@@ -15,74 +15,91 @@
*/ */
package com.google.android.exoplayer.extractor; package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.Format;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* Utility for parsing and representing gapless playback information. * Holder for gapless playback information
*/ */
public final class GaplessInfo { public final class GaplessInfoHolder {
private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN = private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); 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). * The number of samples to trim from the start of the decoded audio stream, or
* {@link Format#NO_VALUE} if not set.
*/
public int encoderDelay;
/**
* The number of samples to trim from the end of the decoded audio stream, or
* {@link Format#NO_VALUE} if not set.
*/
public int encoderPadding;
/**
* Creates a new holder for gapless playback information.
*/
public GaplessInfoHolder() {
encoderDelay = Format.NO_VALUE;
encoderPadding = Format.NO_VALUE;
}
/**
* Populates the holder with data from an MP3 Xing header, if valid and non-zero.
*
* @param value The 24-bit value to parse.
* @return Whether the holder was populated.
*/
public boolean setFromXingHeaderValue(int value) {
int encoderDelay = value >> 12;
int encoderPadding = value & 0x0FFF;
if (encoderDelay > 0 || encoderPadding > 0) {
this.encoderDelay = encoderDelay;
this.encoderPadding = encoderPadding;
return true;
}
return false;
}
/**
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
* or MPEG 4 user data), if valid and non-zero.
* *
* @param name The comment's identifier. * @param name The comment's identifier.
* @param data The comment's payload data. * @param data The comment's payload data.
* @return Parsed gapless playback information, if present and non-zero. {@code null} otherwise. * @return Whether the holder was populated.
*/ */
public static GaplessInfo createFromComment(String name, String data) { public boolean setFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) { if (!GAPLESS_COMMENT_ID.equals(name)) {
return null; return false;
} }
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) { if (matcher.find()) {
try { try {
int encoderDelay = Integer.parseInt(matcher.group(1), 16); int encoderDelay = Integer.parseInt(matcher.group(1), 16);
int encoderPadding = Integer.parseInt(matcher.group(2), 16); int encoderPadding = Integer.parseInt(matcher.group(2), 16);
return encoderDelay == 0 && encoderPadding == 0 ? null if (encoderDelay > 0 || encoderPadding > 0) {
: new GaplessInfo(encoderDelay, encoderPadding); this.encoderDelay = encoderDelay;
this.encoderPadding = encoderPadding;
return true;
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Ignore incorrectly formatted comments. // Ignore incorrectly formatted comments.
} }
} }
return null; return false;
} }
/** /**
* Parses gapless playback information associated with an MP3 Xing header. * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
*
* @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) { public boolean hasGaplessInfo() {
this.encoderDelay = encoderDelay; return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
this.encoderPadding = encoderPadding;
} }
} }
...@@ -16,7 +16,7 @@ ...@@ -16,7 +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.extractor.GaplessInfoHolder;
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;
...@@ -43,15 +43,14 @@ import java.nio.charset.Charset; ...@@ -43,15 +43,14 @@ import java.nio.charset.Charset;
* 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.
* @return The gapless playback information, if present and non-zero. {@code null} otherwise. * @param out The {@link GaplessInfoHolder} to populate.
* @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 GaplessInfo parseId3(ExtractorInput input) public static void parseId3(ExtractorInput input, GaplessInfoHolder out)
throws IOException, InterruptedException { throws IOException, InterruptedException {
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);
...@@ -63,10 +62,10 @@ import java.nio.charset.Charset; ...@@ -63,10 +62,10 @@ import java.nio.charset.Charset;
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 (metadata == null && canParseMetadata(majorVersion, minorVersion, flags, length)) { if (!out.hasGaplessInfo() && 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);
metadata = parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags); parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out);
} else { } else {
input.advancePeekPosition(length); input.advancePeekPosition(length);
} }
...@@ -75,7 +74,6 @@ import java.nio.charset.Charset; ...@@ -75,7 +74,6 @@ import java.nio.charset.Charset;
} }
input.resetPeekPosition(); input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes); input.advancePeekPosition(peekedId3Bytes);
return metadata;
} }
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
...@@ -87,18 +85,19 @@ import java.nio.charset.Charset; ...@@ -87,18 +85,19 @@ import java.nio.charset.Charset;
&& !(majorVersion == 4 && (flags & 0x0F) != 0); && !(majorVersion == 4 && (flags & 0x0F) != 0);
} }
private static GaplessInfo parseGaplessInfo(ParsableByteArray frame, int version, int flags) { private static void parseGaplessInfo(ParsableByteArray frame, int version, int flags,
GaplessInfoHolder 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 null; return;
} }
int extendedHeaderSize = frame.readUnsignedIntToInt(); int extendedHeaderSize = frame.readUnsignedIntToInt();
if (extendedHeaderSize > frame.bytesLeft()) { if (extendedHeaderSize > frame.bytesLeft()) {
return null; return;
} }
int paddingSize; int paddingSize;
if (extendedHeaderSize >= 6) { if (extendedHeaderSize >= 6) {
...@@ -107,17 +106,17 @@ import java.nio.charset.Charset; ...@@ -107,17 +106,17 @@ import java.nio.charset.Charset;
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 null; return;
} }
} }
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 null; return;
} }
int extendedHeaderSize = frame.readSynchSafeInt(); int extendedHeaderSize = frame.readSynchSafeInt();
if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) { if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) {
return null; return;
} }
frame.setPosition(extendedHeaderSize); frame.setPosition(extendedHeaderSize);
} }
...@@ -126,14 +125,11 @@ import java.nio.charset.Charset; ...@@ -126,14 +125,11 @@ import java.nio.charset.Charset;
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) { if (comment.first.length() > 3) {
GaplessInfo gaplessInfo = if (out.setFromComment(comment.first.substring(3), comment.second)) {
GaplessInfo.createFromComment(comment.first.substring(3), comment.second); break;
if (gaplessInfo != null) {
return gaplessInfo;
} }
} }
} }
return null;
} }
private static Pair<String, String> findNextComment(int majorVersion, ParsableByteArray data) { private static Pair<String, String> findNextComment(int majorVersion, ParsableByteArray data) {
......
...@@ -21,7 +21,7 @@ import com.google.android.exoplayer.ParserException; ...@@ -21,7 +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.GaplessInfoHolder;
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;
...@@ -57,6 +57,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -57,6 +57,7 @@ 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 GaplessInfoHolder gaplessInfoHolder;
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
...@@ -64,7 +65,6 @@ public final class Mp3Extractor implements Extractor { ...@@ -64,7 +65,6 @@ 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 long samplesRead; private long samplesRead;
...@@ -87,6 +87,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -87,6 +87,7 @@ 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();
gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = -1; basisTimeUs = -1;
} }
...@@ -124,11 +125,10 @@ public final class Mp3Extractor implements Extractor { ...@@ -124,11 +125,10 @@ public final class Mp3Extractor implements Extractor {
if (seeker == null) { if (seeker == null) {
setupSeeker(input); setupSeeker(input);
extractorOutput.seekMap(seeker); extractorOutput.seekMap(seeker);
int encoderDelay = gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE;
int encoderPadding = gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE;
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, encoderDelay, encoderPadding, null, null)); synchronizedHeader.sampleRate, gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding, null, null));
} }
return readSample(input); return readSample(input);
} }
...@@ -208,7 +208,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -208,7 +208,7 @@ public final class Mp3Extractor implements Extractor {
int peekedId3Bytes = 0; int peekedId3Bytes = 0;
input.resetPeekPosition(); input.resetPeekPosition();
if (input.getPosition() == 0) { if (input.getPosition() == 0) {
gaplessInfo = Id3Util.parseId3(input); Id3Util.parseId3(input, gaplessInfoHolder);
peekedId3Bytes = (int) input.getPeekPosition(); peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) { if (!sniffing) {
input.skipFully(peekedId3Bytes); input.skipFully(peekedId3Bytes);
...@@ -289,13 +289,13 @@ public final class Mp3Extractor implements Extractor { ...@@ -289,13 +289,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 && gaplessInfo == null) { if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// 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);
gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
} }
input.skipFully(synchronizedHeader.frameSize); input.skipFully(synchronizedHeader.frameSize);
} else { } else {
......
...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.extractor.mp4; ...@@ -18,7 +18,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.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.GaplessInfo; import com.google.android.exoplayer.extractor.GaplessInfoHolder;
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;
...@@ -86,11 +86,12 @@ import java.util.List; ...@@ -86,11 +86,12 @@ import java.util.List;
* *
* @param track Track to which this sample table corresponds. * @param track Track to which this sample table corresponds.
* @param stblAtom stbl (sample table) atom to parse. * @param stblAtom stbl (sample table) atom to parse.
* @param gaplessInfoHolder Holder to populate with gapless playback information.
* @return Sample table described by the stbl atom. * @return Sample table described by the stbl atom.
* @throws ParserException If the resulting sample sequence does not contain a sync sample. * @throws ParserException If the resulting sample sequence does not contain a sync sample.
*/ */
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom,
throws ParserException { GaplessInfoHolder gaplessInfoHolder) throws ParserException {
// Array of sample sizes. // Array of sample sizes.
ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;
...@@ -257,15 +258,44 @@ import java.util.List; ...@@ -257,15 +258,44 @@ import java.util.List;
Assertions.checkArgument(remainingTimestampDeltaChanges == 0); Assertions.checkArgument(remainingTimestampDeltaChanges == 0);
Assertions.checkArgument(remainingTimestampOffsetChanges == 0); Assertions.checkArgument(remainingTimestampOffsetChanges == 0);
if (track.editListDurations == null) { if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
// This implementation does not support applying both gapless metadata and an edit list.
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
} }
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that truncate audio and // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
// require prerolling from a sync sample after reordering are not supported. This // sync sample after reordering are not supported. Partial audio sample truncation is only
// implementation handles simple discarding/delaying of samples. The extractor may place // supported in edit lists with one edit that removes less than one sample from the start/end of
// further restrictions on what edited streams are playable. // the track, for gapless audio playback. This implementation handles simple discarding/delaying
// of samples. The extractor may place further restrictions on what edited streams are playable.
if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO
&& timestamps.length >= 2) {
// Handle the edit by setting gapless playback metadata, if possible. This implementation
// assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end
// points of the edit must lie within the first/last samples respectively.
long editStartTime = track.editListMediaTimes[0];
long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
track.timescale, track.movieTimescale);
long lastSampleEndTime = timestampTimeUnits;
if (timestamps[0] <= editStartTime && editStartTime < timestamps[1]
&& timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) {
long paddingTimeUnits = lastSampleEndTime - editEndTime;
long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],
track.format.sampleRate, track.timescale);
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
track.format.sampleRate, track.timescale);
if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE
&& encoderPadding <= Integer.MAX_VALUE) {
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
}
}
}
if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) { if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
// The current version of the spec leaves handling of an edit with zero segment_duration in // The current version of the spec leaves handling of an edit with zero segment_duration in
...@@ -349,13 +379,13 @@ import java.util.List; ...@@ -349,13 +379,13 @@ import java.util.List;
* *
* @param udtaAtom The udta (user data) atom to parse. * @param udtaAtom The udta (user data) atom to parse.
* @param isQuickTime True for QuickTime media. False otherwise. * @param isQuickTime True for QuickTime media. False otherwise.
* @return Gapless playback information stored in the user data, or {@code null} if not present. * @param out {@link GaplessInfoHolder} to populate with gapless playback information.
*/ */
public static GaplessInfo parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { public static void parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, GaplessInfoHolder out) {
if (isQuickTime) { if (isQuickTime) {
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
// parse one. // parse one.
return null; return;
} }
ParsableByteArray udtaData = udtaAtom.data; ParsableByteArray udtaData = udtaAtom.data;
udtaData.setPosition(Atom.HEADER_SIZE); udtaData.setPosition(Atom.HEADER_SIZE);
...@@ -365,15 +395,14 @@ import java.util.List; ...@@ -365,15 +395,14 @@ import java.util.List;
if (atomType == Atom.TYPE_meta) { if (atomType == Atom.TYPE_meta) {
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
udtaData.setLimit(udtaData.getPosition() + atomSize); udtaData.setLimit(udtaData.getPosition() + atomSize);
return parseMetaAtom(udtaData); parseMetaAtom(udtaData, out);
} else { break;
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
} }
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
} }
return null;
} }
private static GaplessInfo parseMetaAtom(ParsableByteArray data) { private static void parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) {
data.skipBytes(Atom.FULL_HEADER_SIZE); data.skipBytes(Atom.FULL_HEADER_SIZE);
ParsableByteArray ilst = new ParsableByteArray(); ParsableByteArray ilst = new ParsableByteArray();
while (data.bytesLeft() >= Atom.HEADER_SIZE) { while (data.bytesLeft() >= Atom.HEADER_SIZE) {
...@@ -382,17 +411,16 @@ import java.util.List; ...@@ -382,17 +411,16 @@ import java.util.List;
if (atomType == Atom.TYPE_ilst) { if (atomType == Atom.TYPE_ilst) {
ilst.reset(data.data, data.getPosition() + payloadSize); ilst.reset(data.data, data.getPosition() + payloadSize);
ilst.setPosition(data.getPosition()); ilst.setPosition(data.getPosition());
GaplessInfo gaplessInfo = parseIlst(ilst); parseIlst(ilst, out);
if (gaplessInfo != null) { if (out.hasGaplessInfo()) {
return gaplessInfo; return;
} }
} }
data.skipBytes(payloadSize); data.skipBytes(payloadSize);
} }
return null;
} }
private static GaplessInfo parseIlst(ParsableByteArray ilst) { private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) {
while (ilst.bytesLeft() > 0) { while (ilst.bytesLeft() > 0) {
int position = ilst.getPosition(); int position = ilst.getPosition();
int endPosition = position + ilst.readInt(); int endPosition = position + ilst.readInt();
...@@ -418,13 +446,13 @@ import java.util.List; ...@@ -418,13 +446,13 @@ import java.util.List;
} }
if (lastCommentName != null && lastCommentData != null if (lastCommentName != null && lastCommentData != null
&& "com.apple.iTunes".equals(lastCommentMean)) { && "com.apple.iTunes".equals(lastCommentMean)) {
return GaplessInfo.createFromComment(lastCommentName, lastCommentData); out.setFromComment(lastCommentName, lastCommentData);
break;
} }
} else { } else {
ilst.setPosition(endPosition); ilst.setPosition(endPosition);
} }
} }
return null;
} }
/** /**
......
...@@ -21,7 +21,7 @@ import com.google.android.exoplayer.ParserException; ...@@ -21,7 +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.GaplessInfoHolder;
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;
...@@ -297,11 +297,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -297,11 +297,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long durationUs = C.UNSET_TIME_US; long durationUs = C.UNSET_TIME_US;
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE; long earliestSampleOffset = Long.MAX_VALUE;
GaplessInfo gaplessInfo = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) { if (udta != null) {
gaplessInfo = AtomParsers.parseUdta(udta, isQuickTime); AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder);
} }
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) {
...@@ -316,7 +318,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -316,7 +318,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom); TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
if (trackSampleTable.sampleCount == 0) { if (trackSampleTable.sampleCount == 0) {
continue; continue;
} }
...@@ -326,8 +328,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -326,8 +328,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Allow ten source samples per output sample, like the platform extractor. // Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10; int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
Format format = track.format.copyWithMaxInputSize(maxInputSize); Format format = track.format.copyWithMaxInputSize(maxInputSize);
if (gaplessInfo != null) { if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) {
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding);
} }
mp4Track.trackOutput.format(format); mp4Track.trackOutput.format(format);
......
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