Apply gapless playback metadata for MP3/MP4 playback using MediaCodec.

Issue: #497
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117219944
This commit is contained in:
olly 2016-03-15 02:42:11 -07:00 committed by Oliver Woodman
parent ba88091c7b
commit a1a48abe92
4 changed files with 74 additions and 28 deletions

View File

@ -126,6 +126,14 @@ public final class Format {
* The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
*/ */
public final int sampleRate; public final int sampleRate;
/**
* 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;
// Text specific. // Text specific.
@ -154,8 +162,8 @@ public final class Format {
String sampleMimeType, int bitrate, int width, int height, float frameRate, String sampleMimeType, int bitrate, int width, int height, float frameRate,
List<byte[]> initializationData) { List<byte[]> initializationData) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, width, height, return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, width, height,
frameRate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, frameRate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null,
initializationData, false); OFFSET_SAMPLE_RELATIVE, initializationData, false);
} }
public static Format createVideoSampleFormat(String id, String sampleMimeType, int bitrate, public static Format createVideoSampleFormat(String id, String sampleMimeType, int bitrate,
@ -168,8 +176,8 @@ public final class Format {
int maxInputSize, int width, int height, float frameRate, List<byte[]> initializationData, int maxInputSize, int width, int height, float frameRate, List<byte[]> initializationData,
int rotationDegrees, float pixelWidthHeightRatio) { int rotationDegrees, float pixelWidthHeightRatio) {
return new Format(id, null, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, return new Format(id, null, sampleMimeType, bitrate, maxInputSize, width, height, frameRate,
rotationDegrees, pixelWidthHeightRatio, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, rotationDegrees, pixelWidthHeightRatio, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null,
initializationData, false); OFFSET_SAMPLE_RELATIVE, initializationData, false);
} }
// Audio. // Audio.
@ -178,16 +186,23 @@ public final class Format {
String sampleMimeType, int bitrate, int channelCount, int sampleRate, String sampleMimeType, int bitrate, int channelCount, int sampleRate,
List<byte[]> initializationData, String language) { List<byte[]> initializationData, String language) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, channelCount, sampleRate, language, OFFSET_SAMPLE_RELATIVE, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, sampleRate, NO_VALUE, NO_VALUE, language,
initializationData, false); OFFSET_SAMPLE_RELATIVE, initializationData, false);
} }
public static Format createAudioSampleFormat(String id, String sampleMimeType, int bitrate, public static Format createAudioSampleFormat(String id, String sampleMimeType, int bitrate,
int maxInputSize, int channelCount, int sampleRate, List<byte[]> initializationData, int maxInputSize, int channelCount, int sampleRate, List<byte[]> initializationData,
String language) { String language) {
return createAudioSampleFormat(id, sampleMimeType, bitrate, maxInputSize, channelCount,
sampleRate, NO_VALUE, NO_VALUE, initializationData, language);
}
public static Format createAudioSampleFormat(String id, String sampleMimeType, int bitrate,
int maxInputSize, int channelCount, int sampleRate, int encoderDelay, int encoderPadding,
List<byte[]> initializationData, String language) {
return new Format(id, null, sampleMimeType, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, channelCount, sampleRate, language, OFFSET_SAMPLE_RELATIVE, NO_VALUE, NO_VALUE, channelCount, sampleRate, encoderDelay, encoderPadding, language,
initializationData, false); OFFSET_SAMPLE_RELATIVE, initializationData, false);
} }
// Text. // Text.
@ -195,8 +210,8 @@ public final class Format {
public static Format createTextContainerFormat(String id, String containerMimeType, public static Format createTextContainerFormat(String id, String containerMimeType,
String sampleMimeType, int bitrate, String language) { String sampleMimeType, int bitrate, String language) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, OFFSET_SAMPLE_RELATIVE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language,
false); OFFSET_SAMPLE_RELATIVE, null, false);
} }
public static Format createTextSampleFormat(String id, String sampleMimeType, int bitrate, public static Format createTextSampleFormat(String id, String sampleMimeType, int bitrate,
@ -207,7 +222,8 @@ public final class Format {
public static Format createTextSampleFormat(String id, String sampleMimeType, int bitrate, public static Format createTextSampleFormat(String id, String sampleMimeType, int bitrate,
String language, long subsampleOffsetUs) { String language, long subsampleOffsetUs) {
return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, subsampleOffsetUs, null, false); NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language,
subsampleOffsetUs, null, false);
} }
// Generic. // Generic.
@ -215,19 +231,21 @@ public final class Format {
public static Format createContainerFormat(String id, String containerMimeType, public static Format createContainerFormat(String id, String containerMimeType,
String sampleMimeType, int bitrate) { String sampleMimeType, int bitrate) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null,
false); OFFSET_SAMPLE_RELATIVE, null, false);
} }
public static Format createSampleFormat(String id, String sampleMimeType, int bitrate) { public static Format createSampleFormat(String id, String sampleMimeType, int bitrate) {
return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, false); NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE,
null, false);
} }
/* package */ Format(String id, String containerMimeType, String sampleMimeType, /* package */ Format(String id, String containerMimeType, String sampleMimeType,
int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees, int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees,
float pixelWidthHeightRatio, int channelCount, int sampleRate, String language, float pixelWidthHeightRatio, int channelCount, int sampleRate, int encoderDelay,
long subsampleOffsetUs, List<byte[]> initializationData, boolean requiresSecureDecryption) { int encoderPadding, String language, long subsampleOffsetUs, List<byte[]> initializationData,
boolean requiresSecureDecryption) {
this.id = id; this.id = id;
this.containerMimeType = containerMimeType; this.containerMimeType = containerMimeType;
this.sampleMimeType = sampleMimeType; this.sampleMimeType = sampleMimeType;
@ -240,6 +258,8 @@ public final class Format {
this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.channelCount = channelCount; this.channelCount = channelCount;
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
this.encoderDelay = encoderDelay;
this.encoderPadding = encoderPadding;
this.language = language; this.language = language;
this.subsampleOffsetUs = subsampleOffsetUs; this.subsampleOffsetUs = subsampleOffsetUs;
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList() this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
@ -250,20 +270,30 @@ public final class Format {
public Format copyWithMaxInputSize(int maxInputSize) { public Format copyWithMaxInputSize(int maxInputSize) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate,
language, subsampleOffsetUs, initializationData, requiresSecureDecryption); encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData,
requiresSecureDecryption);
} }
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate,
language, subsampleOffsetUs, initializationData, requiresSecureDecryption); encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData,
requiresSecureDecryption);
} }
public Format copyWithContainerInfo(String id, int bitrate, int width, int height, public Format copyWithContainerInfo(String id, int bitrate, int width, int height,
String language) { String language) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate,
language, subsampleOffsetUs, initializationData, requiresSecureDecryption); encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData,
requiresSecureDecryption);
}
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate,
encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData,
requiresSecureDecryption);
} }
/** /**
@ -283,6 +313,8 @@ public final class Format {
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
for (int i = 0; i < initializationData.size(); i++) { for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
} }
@ -322,6 +354,8 @@ public final class Format {
result = 31 * result + height; result = 31 * result + height;
result = 31 * result + channelCount; result = 31 * result + channelCount;
result = 31 * result + sampleRate; result = 31 * result + sampleRate;
result = 31 * result + encoderDelay;
result = 31 * result + encoderPadding;
result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + (language == null ? 0 : language.hashCode());
hashCode = result; hashCode = result;
} }
@ -343,6 +377,7 @@ public final class Format {
|| rotationDegrees != other.rotationDegrees || rotationDegrees != other.rotationDegrees
|| pixelWidthHeightRatio != other.pixelWidthHeightRatio || pixelWidthHeightRatio != other.pixelWidthHeightRatio
|| channelCount != other.channelCount || sampleRate != other.sampleRate || channelCount != other.channelCount || sampleRate != other.sampleRate
|| encoderDelay != other.encoderDelay || encoderPadding != other.encoderPadding
|| subsampleOffsetUs != other.subsampleOffsetUs || subsampleOffsetUs != other.subsampleOffsetUs
|| !Util.areEqual(id, other.id) || !Util.areEqual(language, other.language) || !Util.areEqual(id, other.id) || !Util.areEqual(language, other.language)
|| !Util.areEqual(containerMimeType, other.containerMimeType) || !Util.areEqual(containerMimeType, other.containerMimeType)

View File

@ -312,6 +312,8 @@ public final class FrameworkSampleSource implements SampleSource {
int rotationDegrees = getOptionalIntegerV16(mediaFormat, "rotation-degrees"); int rotationDegrees = getOptionalIntegerV16(mediaFormat, "rotation-degrees");
int channelCount = getOptionalIntegerV16(mediaFormat, MediaFormat.KEY_CHANNEL_COUNT); int channelCount = getOptionalIntegerV16(mediaFormat, MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = getOptionalIntegerV16(mediaFormat, MediaFormat.KEY_SAMPLE_RATE); int sampleRate = getOptionalIntegerV16(mediaFormat, MediaFormat.KEY_SAMPLE_RATE);
int encoderDelay = getOptionalIntegerV16(mediaFormat, "encoder-delay");
int encoderPadding = getOptionalIntegerV16(mediaFormat, "encoder-padding");
ArrayList<byte[]> initializationData = new ArrayList<>(); ArrayList<byte[]> initializationData = new ArrayList<>();
for (int i = 0; mediaFormat.containsKey("csd-" + i); i++) { for (int i = 0; mediaFormat.containsKey("csd-" + i); i++) {
ByteBuffer buffer = mediaFormat.getByteBuffer("csd-" + i); ByteBuffer buffer = mediaFormat.getByteBuffer("csd-" + i);
@ -322,7 +324,8 @@ public final class FrameworkSampleSource implements SampleSource {
} }
Format format = new Format(Integer.toString(index), null, mimeType, Format.NO_VALUE, Format format = new Format(Integer.toString(index), null, mimeType, Format.NO_VALUE,
maxInputSize, width, height, frameRate, rotationDegrees, Format.NO_VALUE, channelCount, maxInputSize, width, height, frameRate, rotationDegrees, Format.NO_VALUE, channelCount,
sampleRate, language, Format.OFFSET_SAMPLE_RELATIVE, initializationData, false); sampleRate, encoderDelay, encoderPadding, language, Format.OFFSET_SAMPLE_RELATIVE,
initializationData, false);
format.setFrameworkMediaFormatV16(mediaFormat); format.setFrameworkMediaFormatV16(mediaFormat);
return format; return format;
} }

View File

@ -119,9 +119,11 @@ 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, null, null)); synchronizedHeader.sampleRate, encoderDelay, encoderPadding, null, null));
} }
return readSample(input); return readSample(input);
} }

View File

@ -16,9 +16,11 @@
package com.google.android.exoplayer.extractor.mp4; 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.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.GaplessInfo;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
@ -282,12 +284,12 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long durationUs = C.UNKNOWN_TIME_US; long durationUs = C.UNKNOWN_TIME_US;
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE; long earliestSampleOffset = Long.MAX_VALUE;
// TODO: Apply gapless information. GaplessInfo gaplessInfo = null;
// GaplessInfo gaplessInfo = null; Atom.ContainerAtom udta = moov.getContainerAtomOfType(Atom.TYPE_udta);
// Atom.ContainerAtom udta = moov.getContainerAtomOfType(Atom.TYPE_udta); if (udta != null) {
// if (udta != null) { gaplessInfo = AtomParsers.parseUdta(udta);
// gaplessInfo = AtomParsers.parseUdta(udta); }
// }
for (int i = 0; i < moov.containerChildren.size(); i++) { for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i); Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) { if (atom.type != Atom.TYPE_trak) {
@ -311,7 +313,11 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Each sample has up to three bytes of overhead for the start code that replaces its length. // Each sample has up to three bytes of overhead for the start code that replaces its length.
// 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;
mp4Track.trackOutput.format(track.format.copyWithMaxInputSize(maxInputSize)); Format format = track.format.copyWithMaxInputSize(maxInputSize);
if (gaplessInfo != null) {
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding);
}
mp4Track.trackOutput.format(format);
durationUs = Math.max(durationUs, track.durationUs); durationUs = Math.max(durationUs, track.durationUs);
tracks.add(mp4Track); tracks.add(mp4Track);