Commit 8a89abcb by Oliver Woodman

Merge branch 'dev-v2-id3' into dev-v2

parents 2c543632 8caaf0b5
Showing with 777 additions and 141 deletions
......@@ -27,8 +27,10 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.GeobFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
......@@ -46,7 +48,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
/**
......@@ -55,7 +56,7 @@ import java.util.Locale;
/* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
MetadataRenderer.Output<List<Id3Frame>> {
MetadataRenderer.Output {
private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3;
......@@ -157,6 +158,18 @@ import java.util.Locale;
}
Log.d(TAG, " ]");
}
// Log metadata for at most one of the tracks selected for the renderer.
if (trackSelection != null) {
for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {
Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;
if (metadata != null) {
Log.d(TAG, " Metadata [");
printMetadata(metadata, " ");
Log.d(TAG, " ]");
break;
}
}
}
Log.d(TAG, " ]");
}
}
......@@ -182,34 +195,13 @@ import java.util.Locale;
Log.d(TAG, "]");
}
// MetadataRenderer.Output<List<Id3Frame>>
// MetadataRenderer.Output
@Override
public void onMetadata(List<Id3Frame> id3Frames) {
for (Id3Frame id3Frame : id3Frames) {
if (id3Frame instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id,
txxxFrame.description, txxxFrame.value));
} else if (id3Frame instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner));
} else if (id3Frame instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (id3Frame instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (id3Frame instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
}
}
public void onMetadata(Metadata metadata) {
Log.d(TAG, "onMetadata [");
printMetadata(metadata, " ");
Log.d(TAG, "]");
}
// AudioRendererEventListener
......@@ -354,6 +346,39 @@ import java.util.Locale;
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
}
private void printMetadata(Metadata metadata, String prefix) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id,
txxxFrame.description, txxxFrame.value));
} else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
} else if (entry instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (entry instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id,
commentFrame.language, commentFrame.description, commentFrame.text));
} else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
}
}
}
private String getSessionTimeString() {
return getTimeString(SystemClock.elapsedRealtime() - startTimeMs);
}
......
......@@ -24,6 +24,8 @@ import android.annotation.TargetApi;
import android.media.MediaFormat;
import android.os.Parcel;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
......@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase {
TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3};
Metadata metadata = new Metadata(
new TextInformationFrame("id1", "description1"),
new TextInformationFrame("id2", "description2"));
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,
C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA,
drmInitData);
drmInitData, metadata);
Parcel parcel = Parcel.obtain();
formatToParcel.writeToParcel(parcel, 0);
......
......@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.metadata.id3;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import java.util.List;
import junit.framework.TestCase;
/**
......@@ -30,9 +30,9 @@ public class Id3DecoderTest extends TestCase {
3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54,
54, 52, 95, 115, 116, 97, 114, 116, 0};
Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
}
......@@ -42,9 +42,9 @@ public class Id3DecoderTest extends TestCase {
3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87,
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
ApicFrame apicFrame = (ApicFrame) metadata.get(0);
assertEquals("image/jpeg", apicFrame.mimeType);
assertEquals(16, apicFrame.pictureType);
assertEquals("Hello World", apicFrame.description);
......@@ -56,9 +56,9 @@ public class Id3DecoderTest extends TestCase {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0,
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description);
}
......
......@@ -35,9 +35,9 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
......@@ -107,7 +107,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
private SurfaceHolder surfaceHolder;
private TextureView textureView;
private TextRenderer.Output textOutput;
private MetadataRenderer.Output<List<Id3Frame>> id3Output;
private MetadataRenderer.Output metadataOutput;
private VideoListener videoListener;
private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener;
......@@ -364,12 +364,21 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
/**
* Sets a listener to receive ID3 metadata events.
* @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead.
* @param output The output.
*/
@Deprecated
public void setId3Output(MetadataRenderer.Output output) {
setMetadataOutput(output);
}
/**
* Sets a listener to receive metadata events.
*
* @param output The output.
*/
public void setId3Output(MetadataRenderer.Output<List<Id3Frame>> output) {
id3Output = output;
public void setMetadataOutput(MetadataRenderer.Output output) {
metadataOutput = output;
}
// ExoPlayer implementation
......@@ -540,9 +549,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper());
renderersList.add(textRenderer);
MetadataRenderer<List<Id3Frame>> id3Renderer = new MetadataRenderer<>(componentListener,
MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener,
mainHandler.getLooper(), new Id3Decoder());
renderersList.add(id3Renderer);
renderersList.add(metadataRenderer);
}
private void buildExtensionRenderers(ArrayList<Renderer> renderersList,
......@@ -644,7 +653,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
private final class ComponentListener implements VideoRendererEventListener,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<List<Id3Frame>>,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
// VideoRendererEventListener implementation
......@@ -775,12 +784,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
}
// MetadataRenderer.Output<List<Id3Frame>> implementation
// MetadataRenderer.Output implementation
@Override
public void onMetadata(List<Id3Frame> id3Frames) {
if (id3Output != null) {
id3Output.onMetadata(id3Frames);
public void onMetadata(Metadata metadata) {
if (metadataOutput != null) {
metadataOutput.onMetadata(metadata);
}
}
......
......@@ -16,6 +16,8 @@
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
......@@ -66,6 +68,25 @@ public final class GaplessInfoHolder {
}
/**
* Populates the holder with data parsed from ID3 {@link Metadata}.
*
* @param metadata The metadata from which to parse the gapless information.
* @return Whether the holder was populated.
*/
public boolean setFromMetadata(Metadata metadata) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
if (setFromComment(commentFrame.description, commentFrame.text)) {
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.
*
......@@ -73,7 +94,7 @@ public final class GaplessInfoHolder {
* @param data The comment's payload data.
* @return Whether the holder was populated.
*/
public boolean setFromComment(String name, String data) {
private boolean setFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return false;
}
......
......@@ -27,6 +27,8 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
......@@ -57,6 +59,10 @@ public final class Mp3Extractor implements Extractor {
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/
private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
/**
* Maximum length of data read into {@link #scratch}.
*/
private static final int SCRATCH_LENGTH = 10;
/**
* Mask that includes the audio header values that must match between frames.
......@@ -77,6 +83,7 @@ public final class Mp3Extractor implements Extractor {
private int synchronizedHeaderData;
private Metadata metadata;
private Seeker seeker;
private long basisTimeUs;
private long samplesRead;
......@@ -97,7 +104,7 @@ public final class Mp3Extractor implements Extractor {
*/
public Mp3Extractor(long forcedFirstSampleTimestampUs) {
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(4);
scratch = new ParsableByteArray(SCRATCH_LENGTH);
synchronizedHeader = new MpegAudioHeader();
gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = C.TIME_UNSET;
......@@ -144,7 +151,7 @@ public final class Mp3Extractor implements Extractor {
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding, null, null, 0, null));
gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata));
}
return readSample(input);
}
......@@ -199,7 +206,7 @@ public final class Mp3Extractor implements Extractor {
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition();
if (input.getPosition() == 0) {
Id3Util.parseId3(input, gaplessInfoHolder);
peekId3Data(input);
peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) {
input.skipFully(peekedId3Bytes);
......@@ -254,6 +261,45 @@ public final class Mp3Extractor implements Extractor {
}
/**
* Peeks ID3 data from the input, including gapless playback information.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
int peekedId3Bytes = 0;
while (true) {
input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
// Not an ID3 tag.
break;
}
scratch.skipBytes(3); // Skip major version, minor version and flags.
int framesLength = scratch.readSynchSafeInt();
int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
if (metadata == null) {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
metadata = new Id3Decoder().decode(id3Data, tagLength);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
} else {
input.advancePeekPosition(framesLength);
}
peekedId3Bytes += tagLength;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
}
/**
* Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
* data from the start of the first frame in the stream. On returning, the input's position will
* be set to the start of the first frame of audio.
......
......@@ -132,7 +132,6 @@ import java.util.List;
public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08");
public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
public static final int TYPE_DASHES = Util.getIntegerCodeForString("----");
public final int type;
......@@ -299,7 +298,7 @@ import java.util.List;
* @return The corresponding four character string.
*/
public static String getAtomTypeString(int type) {
return "" + (char) (type >> 24)
return "" + (char) ((type >> 24) & 0xFF)
+ (char) ((type >> 16) & 0xFF)
+ (char) ((type >> 8) & 0xFF)
+ (char) (type & 0xFF);
......
......@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -30,6 +31,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.AvcConfig;
import com.google.android.exoplayer2.video.HevcConfig;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
......@@ -400,80 +402,54 @@ import java.util.List;
*
* @param udtaAtom The udta (user data) atom to decode.
* @param isQuickTime True for QuickTime media. False otherwise.
* @param out {@link GaplessInfoHolder} to populate with gapless playback information.
* @return Parsed metadata, or null.
*/
public static void parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, GaplessInfoHolder out) {
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
if (isQuickTime) {
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
// decode one.
return;
return null;
}
ParsableByteArray udtaData = udtaAtom.data;
udtaData.setPosition(Atom.HEADER_SIZE);
while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
int atomPosition = udtaData.getPosition();
int atomSize = udtaData.readInt();
int atomType = udtaData.readInt();
if (atomType == Atom.TYPE_meta) {
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
udtaData.setLimit(udtaData.getPosition() + atomSize);
parseMetaAtom(udtaData, out);
break;
udtaData.setPosition(atomPosition);
return parseMetaAtom(udtaData, atomPosition + atomSize);
}
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
}
return null;
}
private static void parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) {
data.skipBytes(Atom.FULL_HEADER_SIZE);
ParsableByteArray ilst = new ParsableByteArray();
while (data.bytesLeft() >= Atom.HEADER_SIZE) {
int payloadSize = data.readInt() - Atom.HEADER_SIZE;
int atomType = data.readInt();
private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) {
meta.skipBytes(Atom.FULL_HEADER_SIZE);
while (meta.getPosition() < limit) {
int atomPosition = meta.getPosition();
int atomSize = meta.readInt();
int atomType = meta.readInt();
if (atomType == Atom.TYPE_ilst) {
ilst.reset(data.data, data.getPosition() + payloadSize);
ilst.setPosition(data.getPosition());
parseIlst(ilst, out);
if (out.hasGaplessInfo()) {
return;
}
meta.setPosition(atomPosition);
return parseIlst(meta, atomPosition + atomSize);
}
data.skipBytes(payloadSize);
meta.skipBytes(atomSize - Atom.HEADER_SIZE);
}
return null;
}
private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) {
while (ilst.bytesLeft() > 0) {
int position = ilst.getPosition();
int endPosition = position + ilst.readInt();
int type = ilst.readInt();
if (type == Atom.TYPE_DASHES) {
String lastCommentMean = null;
String lastCommentName = null;
String lastCommentData = null;
while (ilst.getPosition() < endPosition) {
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
int key = ilst.readInt();
ilst.skipBytes(4);
if (key == Atom.TYPE_mean) {
lastCommentMean = ilst.readString(length);
} else if (key == Atom.TYPE_name) {
lastCommentName = ilst.readString(length);
} else if (key == Atom.TYPE_data) {
ilst.skipBytes(4);
lastCommentData = ilst.readString(length - 4);
} else {
ilst.skipBytes(length);
}
}
if (lastCommentName != null && lastCommentData != null
&& "com.apple.iTunes".equals(lastCommentMean)) {
out.setFromComment(lastCommentName, lastCommentData);
break;
}
} else {
ilst.setPosition(endPosition);
private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
ilst.skipBytes(Atom.HEADER_SIZE);
ArrayList<Metadata.Entry> entries = new ArrayList<>();
while (ilst.getPosition() < limit) {
Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);
if (entry != null) {
entries.add(entry);
}
}
return entries.isEmpty() ? null : new Metadata(entries);
}
/**
......@@ -484,12 +460,9 @@ import java.util.List;
*/
private static long parseMvhd(ParsableByteArray mvhd) {
mvhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = mvhd.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
mvhd.skipBytes(version == 0 ? 8 : 16);
return mvhd.readUnsignedInt();
}
......
......@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
......@@ -310,10 +311,14 @@ public final class Mp4Extractor implements Extractor, SeekMap {
List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE;
Metadata metadata = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) {
AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder);
metadata = AtomParsers.parseUdta(udta, isQuickTime);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
}
for (int i = 0; i < moov.containerChildren.size(); i++) {
......@@ -340,9 +345,14 @@ 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 (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) {
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding);
if (track.type == C.TRACK_TYPE_AUDIO) {
if (gaplessInfoHolder.hasGaplessInfo()) {
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding);
}
if (metadata != null) {
format = format.copyWithMetadata(metadata);
}
}
mp4Track.trackOutput.format(format);
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
import java.util.List;
/**
* A collection of metadata entries.
*/
public final class Metadata implements Parcelable {
/**
* A metadata entry.
*/
public interface Entry extends Parcelable {}
private final Entry[] entries;
/**
* @param entries The metadata entries.
*/
public Metadata(Entry... entries) {
this.entries = entries == null ? new Entry[0] : entries;
}
/**
* @param entries The metadata entries.
*/
public Metadata(List<? extends Entry> entries) {
if (entries != null) {
this.entries = new Entry[entries.size()];
entries.toArray(this.entries);
} else {
this.entries = new Entry[0];
}
}
/* package */ Metadata(Parcel in) {
entries = new Metadata.Entry[in.readInt()];
for (int i = 0; i < entries.length; i++) {
entries[i] = in.readParcelable(Entry.class.getClassLoader());
}
}
/**
* Returns the number of metadata entries.
*/
public int length() {
return entries.length;
}
/**
* Returns the entry at the specified index.
*
* @param index The index of the entry.
* @return The entry at the specified index.
*/
public Metadata.Entry get(int index) {
return entries[index];
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Metadata other = (Metadata) obj;
return Arrays.equals(entries, other.entries);
}
@Override
public int hashCode() {
return Arrays.hashCode(entries);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(entries.length);
for (Entry entry : entries) {
dest.writeParcelable(entry, 0);
}
}
public static final Parcelable.Creator<Metadata> CREATOR = new Parcelable.Creator<Metadata>() {
@Override
public Metadata createFromParcel(Parcel in) {
return new Metadata(in);
}
@Override
public Metadata[] newArray(int size) {
return new Metadata[0];
}
};
}
......@@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata;
/**
* Decodes metadata from binary data.
*
* @param <T> The type of the metadata.
*/
public interface MetadataDecoder<T> {
public interface MetadataDecoder {
/**
* Checks whether the decoder supports a given mime type.
......@@ -38,6 +36,6 @@ public interface MetadataDecoder<T> {
* @return The decoded metadata object.
* @throws MetadataDecoderException If a problem occurred decoding the data.
*/
T decode(byte[] data, int size) throws MetadataDecoderException;
Metadata decode(byte[] data, int size) throws MetadataDecoderException;
}
......@@ -30,38 +30,34 @@ import java.nio.ByteBuffer;
/**
* A renderer for metadata.
*
* @param <T> The type of the metadata.
*/
public final class MetadataRenderer<T> extends BaseRenderer implements Callback {
public final class MetadataRenderer extends BaseRenderer implements Callback {
/**
* Receives output from a {@link MetadataRenderer}.
*
* @param <T> The type of the metadata.
*/
public interface Output<T> {
public interface Output {
/**
* Called each time there is a metadata associated with current playback time.
*
* @param metadata The metadata.
*/
void onMetadata(T metadata);
void onMetadata(Metadata metadata);
}
private static final int MSG_INVOKE_RENDERER = 0;
private final MetadataDecoder<T> metadataDecoder;
private final Output<T> output;
private final MetadataDecoder metadataDecoder;
private final Output output;
private final Handler outputHandler;
private final FormatHolder formatHolder;
private final DecoderInputBuffer buffer;
private boolean inputStreamEnded;
private long pendingMetadataTimestamp;
private T pendingMetadata;
private Metadata pendingMetadata;
/**
* @param output The output.
......@@ -72,8 +68,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
* called directly on the player's internal rendering thread.
* @param metadataDecoder A decoder for the metadata.
*/
public MetadataRenderer(Output<T> output, Looper outputLooper,
MetadataDecoder<T> metadataDecoder) {
public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) {
super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output);
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
......@@ -137,7 +132,7 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
return true;
}
private void invokeRenderer(T metadata) {
private void invokeRenderer(Metadata metadata) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else {
......@@ -150,13 +145,13 @@ public final class MetadataRenderer<T> extends BaseRenderer implements Callback
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVOKE_RENDERER:
invokeRendererInternal((T) msg.obj);
invokeRendererInternal((Metadata) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternal(T metadata) {
private void invokeRendererInternal(Metadata metadata) {
output.onMetadata(metadata);
}
......
......@@ -15,6 +15,11 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* APIC (Attached Picture) ID3 frame.
*/
......@@ -35,4 +40,58 @@ public final class ApicFrame extends Id3Frame {
this.pictureData = pictureData;
}
/* package */ ApicFrame(Parcel in) {
super(ID);
mimeType = in.readString();
description = in.readString();
pictureType = in.readInt();
pictureData = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ApicFrame other = (ApicFrame) obj;
return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(description, other.description)
&& Arrays.equals(pictureData, other.pictureData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + pictureType;
result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + Arrays.hashCode(pictureData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mimeType);
dest.writeString(description);
dest.writeInt(pictureType);
dest.writeByteArray(pictureData);
}
public static final Parcelable.Creator<ApicFrame> CREATOR = new Parcelable.Creator<ApicFrame>() {
@Override
public ApicFrame createFromParcel(Parcel in) {
return new ApicFrame(in);
}
@Override
public ApicFrame[] newArray(int size) {
return new ApicFrame[size];
}
};
}
......@@ -15,6 +15,10 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
/**
* Binary ID3 frame.
*/
......@@ -22,9 +26,55 @@ public final class BinaryFrame extends Id3Frame {
public final byte[] data;
public BinaryFrame(String type, byte[] data) {
super(type);
public BinaryFrame(String id, byte[] data) {
super(id);
this.data = data;
}
/* package */ BinaryFrame(Parcel in) {
super(in.readString());
data = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
BinaryFrame other = (BinaryFrame) obj;
return id.equals(other.id) && Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + Arrays.hashCode(data);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeByteArray(data);
}
public static final Parcelable.Creator<BinaryFrame> CREATOR =
new Parcelable.Creator<BinaryFrame>() {
@Override
public BinaryFrame createFromParcel(Parcel in) {
return new BinaryFrame(in);
}
@Override
public BinaryFrame[] newArray(int size) {
return new BinaryFrame[size];
}
};
}
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* Comment ID3 frame.
*/
public final class CommentFrame extends Id3Frame {
public static final String ID = "COMM";
public final String language;
public final String description;
public final String text;
public CommentFrame(String language, String description, String text) {
super(ID);
this.language = language;
this.description = description;
this.text = text;
}
/* package */ CommentFrame(Parcel in) {
super(ID);
language = in.readString();
description = in.readString();
text = in.readString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CommentFrame other = (CommentFrame) obj;
return Util.areEqual(description, other.description) && Util.areEqual(language, other.language)
&& Util.areEqual(text, other.text);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (language != null ? language.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (text != null ? text.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(language);
dest.writeString(text);
}
public static final Parcelable.Creator<CommentFrame> CREATOR =
new Parcelable.Creator<CommentFrame>() {
@Override
public CommentFrame createFromParcel(Parcel in) {
return new CommentFrame(in);
}
@Override
public CommentFrame[] newArray(int size) {
return new CommentFrame[size];
}
};
}
......@@ -15,6 +15,11 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* GEOB (General Encapsulated Object) ID3 frame.
*/
......@@ -35,4 +40,57 @@ public final class GeobFrame extends Id3Frame {
this.data = data;
}
/* package */ GeobFrame(Parcel in) {
super(ID);
mimeType = in.readString();
filename = in.readString();
description = in.readString();
data = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
GeobFrame other = (GeobFrame) obj;
return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename)
&& Util.areEqual(description, other.description) && Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
result = 31 * result + (filename != null ? filename.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + Arrays.hashCode(data);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mimeType);
dest.writeString(filename);
dest.writeString(description);
dest.writeByteArray(data);
}
public static final Parcelable.Creator<GeobFrame> CREATOR = new Parcelable.Creator<GeobFrame>() {
@Override
public GeobFrame createFromParcel(Parcel in) {
return new GeobFrame(in);
}
@Override
public GeobFrame[] newArray(int size) {
return new GeobFrame[size];
}
};
}
......@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
/**
* Base class for ID3 frames.
*/
public abstract class Id3Frame {
public abstract class Id3Frame implements Metadata.Entry {
/**
* The frame ID.
......@@ -26,7 +29,12 @@ public abstract class Id3Frame {
public final String id;
public Id3Frame(String id) {
this.id = id;
this.id = Assertions.checkNotNull(id);
}
@Override
public int describeContents() {
return 0;
}
}
......@@ -15,6 +15,11 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* PRIV (Private) ID3 frame.
*/
......@@ -31,4 +36,50 @@ public final class PrivFrame extends Id3Frame {
this.privateData = privateData;
}
/* package */ PrivFrame(Parcel in) {
super(ID);
owner = in.readString();
privateData = in.createByteArray();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PrivFrame other = (PrivFrame) obj;
return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (owner != null ? owner.hashCode() : 0);
result = 31 * result + Arrays.hashCode(privateData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(owner);
dest.writeByteArray(privateData);
}
public static final Parcelable.Creator<PrivFrame> CREATOR = new Parcelable.Creator<PrivFrame>() {
@Override
public PrivFrame createFromParcel(Parcel in) {
return new PrivFrame(in);
}
@Override
public PrivFrame[] newArray(int size) {
return new PrivFrame[size];
}
};
}
......@@ -15,6 +15,10 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
*/
......@@ -27,4 +31,50 @@ public final class TextInformationFrame extends Id3Frame {
this.description = description;
}
/* package */ TextInformationFrame(Parcel in) {
super(in.readString());
description = in.readString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TextInformationFrame other = (TextInformationFrame) obj;
return id.equals(other.id) && Util.areEqual(description, other.description);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(description);
}
public static final Parcelable.Creator<TextInformationFrame> CREATOR =
new Parcelable.Creator<TextInformationFrame>() {
@Override
public TextInformationFrame createFromParcel(Parcel in) {
return new TextInformationFrame(in);
}
@Override
public TextInformationFrame[] newArray(int size) {
return new TextInformationFrame[size];
}
};
}
......@@ -15,6 +15,10 @@
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* TXXX (User defined text information) ID3 frame.
*/
......@@ -31,4 +35,50 @@ public final class TxxxFrame extends Id3Frame {
this.value = value;
}
/* package */ TxxxFrame(Parcel in) {
super(ID);
description = in.readString();
value = in.readString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TxxxFrame other = (TxxxFrame) obj;
return Util.areEqual(description, other.description) && Util.areEqual(value, other.value);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(description);
dest.writeString(value);
}
public static final Parcelable.Creator<TxxxFrame> CREATOR = new Parcelable.Creator<TxxxFrame>() {
@Override
public TxxxFrame createFromParcel(Parcel in) {
return new TxxxFrame(in);
}
@Override
public TxxxFrame[] newArray(int size) {
return new TxxxFrame[size];
}
};
}
......@@ -300,9 +300,9 @@ public final class ParsableByteArray {
*/
public int readLittleEndianInt() {
return (data[position++] & 0xFF)
| (data[position++] & 0xFF) << 8
| (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 24;
| (data[position++] & 0xFF) << 8
| (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 24;
}
/**
......@@ -424,6 +424,24 @@ public final class ParsableByteArray {
}
/**
* Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is ignored,
* if present.
*
* @param length The number of bytes to read.
* @return The string encoded by the bytes.
*/
public String readNullTerminatedString(int length) {
int stringLength = length;
int lastIndex = position + length - 1;
if (lastIndex < limit && data[lastIndex] == 0) {
stringLength--;
}
String result = new String(data, position, stringLength, Charset.defaultCharset());
position += length;
return result;
}
/**
* Reads the next {@code length} bytes as characters in the specified {@link Charset}.
*
* @param length The number of bytes to read.
......
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