mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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;
|
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) {
|
public boolean hasGaplessInfo() {
|
||||||
int encoderDelay = value >> 12;
|
return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
|
||||||
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,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;
|
||||||
* 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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
&& !(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;
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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;
|
||||||
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;
|
||||||
*
|
*
|
||||||
* @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;
|
||||||
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;
|
||||||
*
|
*
|
||||||
* @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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
// 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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue