mirror of
https://github.com/samsonjs/media.git
synced 2026-04-24 14:37:45 +00:00
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
This commit is contained in:
parent
23cb9532c5
commit
9b57487845
5 changed files with 137 additions and 93 deletions
|
|
@ -15,74 +15,91 @@
|
|||
*/
|
||||
package com.google.android.exoplayer.extractor;
|
||||
|
||||
import com.google.android.exoplayer.Format;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
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 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).
|
||||
* 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 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)) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
this.encoderPadding = encoderPadding;
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore incorrectly formatted comments.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
|
||||
*/
|
||||
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;
|
||||
public boolean hasGaplessInfo() {
|
||||
return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,7 +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.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
|
|
@ -43,15 +43,14 @@ import java.nio.charset.Charset;
|
|||
* Peeks data from the input and parses ID3 metadata.
|
||||
*
|
||||
* @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 InterruptedException If the thread was interrupted.
|
||||
*/
|
||||
public static GaplessInfo parseId3(ExtractorInput input)
|
||||
public static void parseId3(ExtractorInput input, GaplessInfoHolder out)
|
||||
throws IOException, InterruptedException {
|
||||
ParsableByteArray scratch = new ParsableByteArray(10);
|
||||
int peekedId3Bytes = 0;
|
||||
GaplessInfo metadata = null;
|
||||
while (true) {
|
||||
input.peekFully(scratch.data, 0, 10);
|
||||
scratch.setPosition(0);
|
||||
|
|
@ -63,10 +62,10 @@ import java.nio.charset.Charset;
|
|||
int minorVersion = scratch.readUnsignedByte();
|
||||
int flags = scratch.readUnsignedByte();
|
||||
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];
|
||||
input.peekFully(frame, 0, length);
|
||||
metadata = parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags);
|
||||
parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out);
|
||||
} else {
|
||||
input.advancePeekPosition(length);
|
||||
}
|
||||
|
|
@ -75,7 +74,6 @@ import java.nio.charset.Charset;
|
|||
}
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(peekedId3Bytes);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
|
||||
|
|
@ -87,18 +85,19 @@ import java.nio.charset.Charset;
|
|||
&& !(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);
|
||||
|
||||
// Skip any extended header.
|
||||
frame.setPosition(0);
|
||||
if (version == 3 && (flags & 0x40) != 0) {
|
||||
if (frame.bytesLeft() < 4) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
int extendedHeaderSize = frame.readUnsignedIntToInt();
|
||||
if (extendedHeaderSize > frame.bytesLeft()) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
int paddingSize;
|
||||
if (extendedHeaderSize >= 6) {
|
||||
|
|
@ -107,17 +106,17 @@ import java.nio.charset.Charset;
|
|||
frame.setPosition(4);
|
||||
frame.setLimit(frame.limit() - paddingSize);
|
||||
if (frame.bytesLeft() < extendedHeaderSize) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
frame.skipBytes(extendedHeaderSize);
|
||||
} else if (version == 4 && (flags & 0x40) != 0) {
|
||||
if (frame.bytesLeft() < 4) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
int extendedHeaderSize = frame.readSynchSafeInt();
|
||||
if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
frame.setPosition(extendedHeaderSize);
|
||||
}
|
||||
|
|
@ -126,14 +125,11 @@ import java.nio.charset.Charset;
|
|||
Pair<String, String> comment;
|
||||
while ((comment = findNextComment(version, frame)) != null) {
|
||||
if (comment.first.length() > 3) {
|
||||
GaplessInfo gaplessInfo =
|
||||
GaplessInfo.createFromComment(comment.first.substring(3), comment.second);
|
||||
if (gaplessInfo != null) {
|
||||
return gaplessInfo;
|
||||
if (out.setFromComment(comment.first.substring(3), comment.second)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Pair<String, String> findNextComment(int majorVersion, ParsableByteArray data) {
|
||||
|
|
|
|||
|
|
@ -21,7 +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.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer.extractor.SeekMap;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
|
|
@ -57,6 +57,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
private final long forcedFirstSampleTimestampUs;
|
||||
private final ParsableByteArray scratch;
|
||||
private final MpegAudioHeader synchronizedHeader;
|
||||
private final GaplessInfoHolder gaplessInfoHolder;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
|
|
@ -64,7 +65,6 @@ public final class Mp3Extractor implements Extractor {
|
|||
|
||||
private int synchronizedHeaderData;
|
||||
|
||||
private GaplessInfo gaplessInfo;
|
||||
private Seeker seeker;
|
||||
private long basisTimeUs;
|
||||
private long samplesRead;
|
||||
|
|
@ -87,6 +87,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(4);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
gaplessInfoHolder = new GaplessInfoHolder();
|
||||
basisTimeUs = -1;
|
||||
}
|
||||
|
||||
|
|
@ -124,11 +125,10 @@ public final class Mp3Extractor implements Extractor {
|
|||
if (seeker == null) {
|
||||
setupSeeker(input);
|
||||
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,
|
||||
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);
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
int peekedId3Bytes = 0;
|
||||
input.resetPeekPosition();
|
||||
if (input.getPosition() == 0) {
|
||||
gaplessInfo = Id3Util.parseId3(input);
|
||||
Id3Util.parseId3(input, gaplessInfoHolder);
|
||||
peekedId3Bytes = (int) input.getPeekPosition();
|
||||
if (!sniffing) {
|
||||
input.skipFully(peekedId3Bytes);
|
||||
|
|
@ -289,13 +289,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 && gaplessInfo == null) {
|
||||
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
|
||||
// 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);
|
||||
gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24());
|
||||
gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
|
||||
}
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,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.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.Assertions;
|
||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||
|
|
@ -86,11 +86,12 @@ import java.util.List;
|
|||
*
|
||||
* @param track Track to which this sample table corresponds.
|
||||
* @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.
|
||||
* @throws ParserException If the resulting sample sequence does not contain a sync sample.
|
||||
*/
|
||||
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom)
|
||||
throws ParserException {
|
||||
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom,
|
||||
GaplessInfoHolder gaplessInfoHolder) throws ParserException {
|
||||
// Array of sample sizes.
|
||||
ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;
|
||||
|
||||
|
|
@ -257,15 +258,44 @@ import java.util.List;
|
|||
Assertions.checkArgument(remainingTimestampDeltaChanges == 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);
|
||||
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
|
||||
// require prerolling from a sync sample after reordering are not supported. This
|
||||
// implementation handles simple discarding/delaying of samples. The extractor may place
|
||||
// further restrictions on what edited streams are playable.
|
||||
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
|
||||
// sync sample after reordering are not supported. Partial audio sample truncation is only
|
||||
// supported in edit lists with one edit that removes less than one sample from the start/end of
|
||||
// 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) {
|
||||
// The current version of the spec leaves handling of an edit with zero segment_duration in
|
||||
|
|
@ -349,13 +379,13 @@ import java.util.List;
|
|||
*
|
||||
* @param udtaAtom The udta (user data) atom to parse.
|
||||
* @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) {
|
||||
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
|
||||
// parse one.
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
ParsableByteArray udtaData = udtaAtom.data;
|
||||
udtaData.setPosition(Atom.HEADER_SIZE);
|
||||
|
|
@ -365,15 +395,14 @@ import java.util.List;
|
|||
if (atomType == Atom.TYPE_meta) {
|
||||
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
|
||||
udtaData.setLimit(udtaData.getPosition() + atomSize);
|
||||
return parseMetaAtom(udtaData);
|
||||
} else {
|
||||
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
|
||||
parseMetaAtom(udtaData, out);
|
||||
break;
|
||||
}
|
||||
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);
|
||||
ParsableByteArray ilst = new ParsableByteArray();
|
||||
while (data.bytesLeft() >= Atom.HEADER_SIZE) {
|
||||
|
|
@ -382,17 +411,16 @@ import java.util.List;
|
|||
if (atomType == Atom.TYPE_ilst) {
|
||||
ilst.reset(data.data, data.getPosition() + payloadSize);
|
||||
ilst.setPosition(data.getPosition());
|
||||
GaplessInfo gaplessInfo = parseIlst(ilst);
|
||||
if (gaplessInfo != null) {
|
||||
return gaplessInfo;
|
||||
parseIlst(ilst, out);
|
||||
if (out.hasGaplessInfo()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.skipBytes(payloadSize);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static GaplessInfo parseIlst(ParsableByteArray ilst) {
|
||||
private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) {
|
||||
while (ilst.bytesLeft() > 0) {
|
||||
int position = ilst.getPosition();
|
||||
int endPosition = position + ilst.readInt();
|
||||
|
|
@ -418,13 +446,13 @@ import java.util.List;
|
|||
}
|
||||
if (lastCommentName != null && lastCommentData != null
|
||||
&& "com.apple.iTunes".equals(lastCommentMean)) {
|
||||
return GaplessInfo.createFromComment(lastCommentName, lastCommentData);
|
||||
out.setFromComment(lastCommentName, lastCommentData);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ilst.setPosition(endPosition);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,7 +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.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer.extractor.SeekMap;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
|
|
@ -297,11 +297,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
long durationUs = C.UNSET_TIME_US;
|
||||
List<Mp4Track> tracks = new ArrayList<>();
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
GaplessInfo gaplessInfo = null;
|
||||
|
||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
|
||||
if (udta != null) {
|
||||
gaplessInfo = AtomParsers.parseUdta(udta, isQuickTime);
|
||||
AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder);
|
||||
}
|
||||
|
||||
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
||||
Atom.ContainerAtom atom = moov.containerChildren.get(i);
|
||||
if (atom.type != Atom.TYPE_trak) {
|
||||
|
|
@ -316,7 +318,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
|
||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||
.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) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -326,8 +328,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
// Allow ten source samples per output sample, like the platform extractor.
|
||||
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
||||
Format format = track.format.copyWithMaxInputSize(maxInputSize);
|
||||
if (gaplessInfo != null) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding);
|
||||
if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding);
|
||||
}
|
||||
mp4Track.trackOutput.format(format);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue