mirror of
https://github.com/samsonjs/media.git
synced 2026-04-04 11:05:47 +00:00
Further modifications to ID3 support
- Lots of misc cleanup - Remove GaplessInfo from Metadata. IMO it doesn't quite belong there, and means it ends up being represented twice inside Format. - Note: Changes untested, but will be tested in due course!
This commit is contained in:
parent
3b34f850f2
commit
ba1da140c6
16 changed files with 318 additions and 382 deletions
|
|
@ -27,7 +27,6 @@ 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.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
|
|
@ -45,12 +44,10 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
|||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelections;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
|
@ -59,7 +56,7 @@ import java.util.Locale;
|
|||
/* package */ final class EventLogger implements ExoPlayer.EventListener,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
|
||||
MappingTrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output<Metadata> {
|
||||
MappingTrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final int MAX_TIMELINE_ITEM_LINES = 3;
|
||||
|
|
@ -179,44 +176,40 @@ import java.util.Locale;
|
|||
Log.d(TAG, "]");
|
||||
}
|
||||
|
||||
// MetadataRenderer.Output<Metadata>
|
||||
// MetadataRenderer.Output
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||
for (Id3Frame id3Frame : id3Frames) {
|
||||
if (id3Frame instanceof TxxxFrame) {
|
||||
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof TxxxFrame) {
|
||||
TxxxFrame txxxFrame = (TxxxFrame) entry;
|
||||
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;
|
||||
} else if (entry instanceof PrivFrame) {
|
||||
PrivFrame privFrame = (PrivFrame) entry;
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner));
|
||||
} else if (id3Frame instanceof GeobFrame) {
|
||||
GeobFrame geobFrame = (GeobFrame) id3Frame;
|
||||
} else if (entry instanceof GeobFrame) {
|
||||
GeobFrame geobFrame = (GeobFrame) entry;
|
||||
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;
|
||||
} else if (entry instanceof ApicFrame) {
|
||||
ApicFrame apicFrame = (ApicFrame) entry;
|
||||
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;
|
||||
} else if (entry instanceof TextInformationFrame) {
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
|
||||
textInformationFrame.description));
|
||||
} else if (id3Frame instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) id3Frame;
|
||||
} else if (entry instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) entry;
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id,
|
||||
commentFrame.language, commentFrame.text));
|
||||
} else {
|
||||
} else if (entry instanceof Id3Frame) {
|
||||
Id3Frame id3Frame = (Id3Frame) entry;
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
|
||||
}
|
||||
}
|
||||
GaplessInfo gaplessInfo = metadata.getGaplessInfo();
|
||||
if (gaplessInfo != null) {
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d",
|
||||
gaplessInfo.encoderDelay, gaplessInfo.encoderPadding));
|
||||
}
|
||||
}
|
||||
|
||||
// AudioRendererEventListener
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -32,9 +32,8 @@ public class Id3DecoderTest extends TestCase {
|
|||
54, 52, 95, 115, 116, 97, 114, 116, 0};
|
||||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||
assertEquals(1, id3Frames.size());
|
||||
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
|
||||
assertEquals(1, metadata.length());
|
||||
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
|
||||
assertEquals("", txxxFrame.description);
|
||||
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
|
||||
}
|
||||
|
|
@ -45,9 +44,8 @@ public class Id3DecoderTest extends TestCase {
|
|||
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
|
||||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||
assertEquals(1, id3Frames.size());
|
||||
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
|
||||
assertEquals(1, metadata.length());
|
||||
ApicFrame apicFrame = (ApicFrame) metadata.get(0);
|
||||
assertEquals("image/jpeg", apicFrame.mimeType);
|
||||
assertEquals(16, apicFrame.pictureType);
|
||||
assertEquals("Hello World", apicFrame.description);
|
||||
|
|
@ -60,9 +58,8 @@ public class Id3DecoderTest extends TestCase {
|
|||
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
|
||||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
List<Id3Frame> id3Frames = metadata.getFrames();
|
||||
assertEquals(1, id3Frames.size());
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
|
||||
assertEquals(1, metadata.length());
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
|
||||
assertEquals("TIT2", textInformationFrame.id);
|
||||
assertEquals("Hello World", textInformationFrame.description);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import android.media.MediaFormat;
|
|||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
|
@ -411,7 +410,7 @@ public final class Format implements Parcelable {
|
|||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
|
||||
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
|
||||
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
|
||||
language, subsampleOffsetUs, initializationData, drmInitData, null);
|
||||
language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
}
|
||||
|
||||
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
|
||||
|
|
@ -429,14 +428,10 @@ public final class Format implements Parcelable {
|
|||
}
|
||||
|
||||
public Format copyWithMetadata(Metadata metadata) {
|
||||
GaplessInfo gaplessInfo = metadata.getGaplessInfo();
|
||||
int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay;
|
||||
int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding;
|
||||
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
|
||||
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
|
||||
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
private SurfaceHolder surfaceHolder;
|
||||
private TextureView textureView;
|
||||
private TextRenderer.Output textOutput;
|
||||
private MetadataRenderer.Output<Metadata> id3Output;
|
||||
private MetadataRenderer.Output metadataOutput;
|
||||
private VideoListener videoListener;
|
||||
private AudioRendererEventListener audioDebugListener;
|
||||
private VideoRendererEventListener videoDebugListener;
|
||||
|
|
@ -389,12 +389,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<Metadata> output) {
|
||||
id3Output = output;
|
||||
public void setMetadataOutput(MetadataRenderer.Output output) {
|
||||
metadataOutput = output;
|
||||
}
|
||||
|
||||
// ExoPlayer implementation
|
||||
|
|
@ -539,9 +548,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper());
|
||||
renderersList.add(textRenderer);
|
||||
|
||||
MetadataRenderer<Metadata> 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,
|
||||
|
|
@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
|
||||
private final class ComponentListener implements VideoRendererEventListener,
|
||||
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<Metadata>,
|
||||
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
|
||||
SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
|
||||
TrackSelector.EventListener<Object> {
|
||||
|
||||
|
|
@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
// MetadataRenderer.Output<Metadata> implementation
|
||||
// MetadataRenderer.Output implementation
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
if (id3Output != null) {
|
||||
id3Output.onMetadata(metadata);
|
||||
if (metadataOutput != null) {
|
||||
metadataOutput.onMetadata(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* 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.extractor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Gapless playback information.
|
||||
*/
|
||||
public final class GaplessInfo {
|
||||
|
||||
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
|
||||
private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Parses gapless playback information from a gapless playback comment (stored in an ID3 header
|
||||
* or MPEG 4 user data), if valid and non-zero.
|
||||
* @param name The comment's identifier.
|
||||
* @param data The comment's payload data.
|
||||
* @return the gapless playback info, or null if the provided data is not valid.
|
||||
*/
|
||||
public static GaplessInfo createFromComment(String name, String data) {
|
||||
if(!GAPLESS_COMMENT_ID.equals(name)) {
|
||||
return null;
|
||||
} else {
|
||||
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
|
||||
if(matcher.find()) {
|
||||
try {
|
||||
int encoderDelay = Integer.parseInt(matcher.group(1), 16);
|
||||
int encoderPadding = Integer.parseInt(matcher.group(2), 16);
|
||||
if(encoderDelay > 0 || encoderPadding > 0) {
|
||||
Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding);
|
||||
return new GaplessInfo(encoderDelay, encoderPadding);
|
||||
}
|
||||
} catch (NumberFormatException var5) {
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore incorrectly formatted comments.
|
||||
Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses gapless playback information from an MP3 Xing header, if valid and non-zero.
|
||||
*
|
||||
* @param value The 24-bit value to decode.
|
||||
* @return the gapless playback info, or null if the provided data is not valid.
|
||||
*/
|
||||
public static GaplessInfo createFromXingHeaderValue(int value) {
|
||||
int encoderDelay = value >> 12;
|
||||
int encoderPadding = value & 0x0FFF;
|
||||
return encoderDelay > 0 || encoderPadding > 0 ?
|
||||
new GaplessInfo(encoderDelay, encoderPadding) :
|
||||
null;
|
||||
}
|
||||
|
||||
public GaplessInfo(int encoderDelay, int encoderPadding) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
this.encoderPadding = encoderPadding;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,91 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Holder for gapless playback information.
|
||||
*/
|
||||
public final class GaplessInfoHolder {
|
||||
|
||||
public GaplessInfo gaplessInfo;
|
||||
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
|
||||
private static final Pattern GAPLESS_COMMENT_PATTERN =
|
||||
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
|
||||
|
||||
/**
|
||||
* 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 decode.
|
||||
* @return Whether the holder was populated.
|
||||
*/
|
||||
public boolean setFromXingHeaderValue(int value) {
|
||||
int encoderDelay = value >> 12;
|
||||
int encoderPadding = value & 0x0FFF;
|
||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
this.encoderPadding = encoderPadding;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
|
||||
* or MPEG 4 user data), if valid and non-zero.
|
||||
*
|
||||
* @param name The comment's identifier.
|
||||
* @param data The comment's payload data.
|
||||
* @return Whether the holder was populated.
|
||||
*/
|
||||
public boolean setFromComment(String name, String data) {
|
||||
if (!GAPLESS_COMMENT_ID.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
int encoderDelay = Integer.parseInt(matcher.group(1), 16);
|
||||
int encoderPadding = Integer.parseInt(matcher.group(2), 16);
|
||||
if (encoderDelay > 0 || encoderPadding > 0) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
this.encoderPadding = encoderPadding;
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore incorrectly formatted comments.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
|
||||
*/
|
||||
public boolean hasGaplessInfo() {
|
||||
return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,18 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
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.CommentFrame;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import org.w3c.dom.Comment;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
@ -70,7 +74,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
private final long forcedFirstSampleTimestampUs;
|
||||
private final ParsableByteArray scratch;
|
||||
private final MpegAudioHeader synchronizedHeader;
|
||||
private Metadata metadata;
|
||||
private final GaplessInfoHolder gaplessInfoHolder;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
|
|
@ -78,6 +82,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
|
||||
private int synchronizedHeaderData;
|
||||
|
||||
private Metadata metadata;
|
||||
private Seeker seeker;
|
||||
private long basisTimeUs;
|
||||
private long samplesRead;
|
||||
|
|
@ -100,6 +105,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(4);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
gaplessInfoHolder = new GaplessInfoHolder();
|
||||
basisTimeUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
|
|
@ -141,20 +147,13 @@ public final class Mp3Extractor implements Extractor {
|
|||
if (seeker == null) {
|
||||
seeker = setupSeeker(input);
|
||||
extractorOutput.seekMap(seeker);
|
||||
|
||||
GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null;
|
||||
|
||||
Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||
synchronizedHeader.sampleRate, Format.NO_VALUE,
|
||||
gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE,
|
||||
gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE,
|
||||
null, null, 0, null);
|
||||
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding, null, null, 0, null);
|
||||
if (metadata != null) {
|
||||
format = format.copyWithMetadata(metadata);
|
||||
}
|
||||
|
||||
trackOutput.format(format);
|
||||
}
|
||||
return readSample(input);
|
||||
|
|
@ -211,6 +210,17 @@ public final class Mp3Extractor implements Extractor {
|
|||
input.resetPeekPosition();
|
||||
if (input.getPosition() == 0) {
|
||||
metadata = Id3Util.parseId3(input);
|
||||
if (!gaplessInfoHolder.hasGaplessInfo()) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) entry;
|
||||
if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
peekedId3Bytes = (int) input.getPeekPosition();
|
||||
if (!sniffing) {
|
||||
input.skipFully(peekedId3Bytes);
|
||||
|
|
@ -296,16 +306,13 @@ public final class Mp3Extractor implements Extractor {
|
|||
}
|
||||
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
||||
seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
|
||||
if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) {
|
||||
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
|
||||
// If there is a Xing header, read gapless playback metadata at a fixed offset.
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(xingBase + 141);
|
||||
input.peekFully(scratch.data, 0, 3);
|
||||
scratch.setPosition(0);
|
||||
GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24());
|
||||
metadata = metadata != null ?
|
||||
metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo);
|
||||
|
||||
gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
|
||||
}
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
} else if (frame.limit() >= 40) {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,8 @@ import com.google.android.exoplayer2.Format;
|
|||
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.GaplessInfo;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataBuilder;
|
||||
import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
|
|
@ -38,6 +36,8 @@ 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;
|
||||
|
|
@ -95,8 +95,8 @@ import java.util.List;
|
|||
Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
|
||||
return stsdData.format == null ? null
|
||||
: new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
|
||||
stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
|
||||
stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second);
|
||||
stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
|
||||
stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -286,7 +286,7 @@ import java.util.List;
|
|||
flags = rechunkedResults.flags;
|
||||
}
|
||||
|
||||
if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != 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);
|
||||
|
|
@ -315,9 +315,10 @@ import java.util.List;
|
|||
track.format.sampleRate, track.timescale);
|
||||
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
|
||||
track.format.sampleRate, track.timescale);
|
||||
if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE
|
||||
if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE
|
||||
&& encoderPadding <= Integer.MAX_VALUE) {
|
||||
gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding);
|
||||
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);
|
||||
}
|
||||
|
|
@ -402,13 +403,14 @@ import java.util.List;
|
|||
}
|
||||
|
||||
/**
|
||||
* Parses a udta atom for metadata, including gapless playback information.
|
||||
* Parses a udta atom.
|
||||
*
|
||||
* @param udtaAtom The udta (user data) atom to decode.
|
||||
* @param isQuickTime True for QuickTime media. False otherwise.
|
||||
* @return metadata stored in the user data, or {@code null} if not present.
|
||||
* @param out {@link GaplessInfoHolder} to populate with gapless playback information.
|
||||
*/
|
||||
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
|
||||
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime,
|
||||
GaplessInfoHolder out) {
|
||||
if (isQuickTime) {
|
||||
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
|
||||
// decode one.
|
||||
|
|
@ -422,14 +424,14 @@ import java.util.List;
|
|||
if (atomType == Atom.TYPE_meta) {
|
||||
udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE);
|
||||
udtaData.setLimit(udtaData.getPosition() + atomSize);
|
||||
return parseMetaAtom(udtaData);
|
||||
return parseMetaAtom(udtaData, out);
|
||||
}
|
||||
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Metadata parseMetaAtom(ParsableByteArray data) {
|
||||
private static Metadata parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) {
|
||||
data.skipBytes(Atom.FULL_HEADER_SIZE);
|
||||
ParsableByteArray ilst = new ParsableByteArray();
|
||||
while (data.bytesLeft() >= Atom.HEADER_SIZE) {
|
||||
|
|
@ -438,9 +440,9 @@ import java.util.List;
|
|||
if (atomType == Atom.TYPE_ilst) {
|
||||
ilst.reset(data.data, data.getPosition() + payloadSize);
|
||||
ilst.setPosition(data.getPosition());
|
||||
Metadata result = parseIlst(ilst);
|
||||
if (result != null) {
|
||||
return result;
|
||||
Metadata metadata = parseIlst(ilst, out);
|
||||
if (metadata != null) {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
data.skipBytes(payloadSize);
|
||||
|
|
@ -448,19 +450,16 @@ import java.util.List;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static Metadata parseIlst(ParsableByteArray ilst) {
|
||||
|
||||
MetadataBuilder builder = new MetadataBuilder();
|
||||
|
||||
private static Metadata parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) {
|
||||
ArrayList<Metadata.Entry> entries = new ArrayList<>();
|
||||
while (ilst.bytesLeft() > 0) {
|
||||
int position = ilst.getPosition();
|
||||
int endPosition = position + ilst.readInt();
|
||||
int type = ilst.readInt();
|
||||
parseIlstElement(ilst, type, endPosition, builder);
|
||||
parseIlstElement(ilst, type, endPosition, entries, out);
|
||||
ilst.setPosition(endPosition);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
return entries.isEmpty() ? null : new Metadata(entries);
|
||||
}
|
||||
|
||||
private static final String P1 = "\u00a9";
|
||||
|
|
@ -506,66 +505,64 @@ import java.util.List;
|
|||
|
||||
// TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes
|
||||
|
||||
private static void parseIlstElement(
|
||||
ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) {
|
||||
private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition,
|
||||
List<Metadata.Entry> builder, GaplessInfoHolder out) {
|
||||
if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) {
|
||||
parseTextAttribute(builder, "TIT2", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TIT2", ilst);
|
||||
} else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) {
|
||||
parseCommentAttribute(builder, "COMM", ilst, endPosition);
|
||||
parseCommentAttribute(builder, "COMM", ilst);
|
||||
} else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) {
|
||||
parseTextAttribute(builder, "TDRC", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TDRC", ilst);
|
||||
} else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) {
|
||||
parseTextAttribute(builder, "TPE1", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TPE1", ilst);
|
||||
} else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) {
|
||||
parseTextAttribute(builder, "TSSE", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TSSE", ilst);
|
||||
} else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) {
|
||||
parseTextAttribute(builder, "TALB", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TALB", ilst);
|
||||
} else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 ||
|
||||
type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) {
|
||||
parseTextAttribute(builder, "TCOM", ilst, endPosition);
|
||||
type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) {
|
||||
parseTextAttribute(builder, "TCOM", ilst);
|
||||
} else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) {
|
||||
parseTextAttribute(builder, "lyrics", ilst, endPosition);
|
||||
parseTextAttribute(builder, "lyrics", ilst);
|
||||
} else if (type == TYPE_STANDARD_GENRE) {
|
||||
parseStandardGenreAttribute(builder, "TCON", ilst, endPosition);
|
||||
parseStandardGenreAttribute(builder, "TCON", ilst);
|
||||
} else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) {
|
||||
parseTextAttribute(builder, "TCON", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TCON", ilst);
|
||||
} else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) {
|
||||
parseTextAttribute(builder, "TIT1", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TIT1", ilst);
|
||||
} else if (type == TYPE_DISK_NUMBER) {
|
||||
parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition);
|
||||
} else if (type == TYPE_TRACK_NUMBER) {
|
||||
parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition);
|
||||
} else if (type == TYPE_TEMPO) {
|
||||
parseIntegerAttribute(builder, "TBPM", ilst, endPosition);
|
||||
parseIntegerAttribute(builder, "TBPM", ilst);
|
||||
} else if (type == TYPE_COMPILATION) {
|
||||
parseBooleanAttribute(builder, "TCMP", ilst, endPosition);
|
||||
parseBooleanAttribute(builder, "TCMP", ilst);
|
||||
} else if (type == TYPE_ALBUM_ARTIST) {
|
||||
parseTextAttribute(builder, "TPE2", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TPE2", ilst);
|
||||
} else if (type == TYPE_SORT_TRACK_NAME) {
|
||||
parseTextAttribute(builder, "TSOT", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TSOT", ilst);
|
||||
} else if (type == TYPE_SORT_ALBUM) {
|
||||
parseTextAttribute(builder, "TSO2", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TSO2", ilst);
|
||||
} else if (type == TYPE_SORT_ARTIST) {
|
||||
parseTextAttribute(builder, "TSOA", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TSOA", ilst);
|
||||
} else if (type == TYPE_SORT_ALBUM_ARTIST) {
|
||||
parseTextAttribute(builder, "TSOP", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TSOP", ilst);
|
||||
} else if (type == TYPE_SORT_COMPOSER) {
|
||||
parseTextAttribute(builder, "TSOC", ilst, endPosition);
|
||||
parseTextAttribute(builder, "TSOC", ilst);
|
||||
} else if (type == TYPE_SORT_SHOW) {
|
||||
parseTextAttribute(builder, "sortShow", ilst, endPosition);
|
||||
parseTextAttribute(builder, "sortShow", ilst);
|
||||
} else if (type == TYPE_GAPLESS_ALBUM) {
|
||||
parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition);
|
||||
parseBooleanAttribute(builder, "gaplessAlbum", ilst);
|
||||
} else if (type == TYPE_SHOW) {
|
||||
parseTextAttribute(builder, "show", ilst, endPosition);
|
||||
parseTextAttribute(builder, "show", ilst);
|
||||
} else if (type == Atom.TYPE_DASHES) {
|
||||
parseExtendedAttribute(builder, ilst, endPosition);
|
||||
parseExtendedAttribute(builder, ilst, endPosition, out);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseTextAttribute(MetadataBuilder builder,
|
||||
String attributeName,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseTextAttribute(List<Metadata.Entry> builder, String attributeName,
|
||||
ParsableByteArray ilst) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
|
|
@ -579,10 +576,8 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private static void parseCommentAttribute(MetadataBuilder builder,
|
||||
String attributeName,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseCommentAttribute(List<Metadata.Entry> builder, String attributeName,
|
||||
ParsableByteArray ilst) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
|
|
@ -596,10 +591,8 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private static void parseBooleanAttribute(MetadataBuilder builder,
|
||||
String attributeName,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseBooleanAttribute(List<Metadata.Entry> builder, String attributeName,
|
||||
ParsableByteArray ilst) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
|
|
@ -616,10 +609,8 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private static void parseIntegerAttribute(MetadataBuilder builder,
|
||||
String attributeName,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseIntegerAttribute(List<Metadata.Entry> builder, String attributeName,
|
||||
ParsableByteArray ilst) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
|
|
@ -636,10 +627,8 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private static void parseIndexAndCountAttribute(MetadataBuilder builder,
|
||||
String attributeName,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseIndexAndCountAttribute(List<Metadata.Entry> builder,
|
||||
String attributeName, ParsableByteArray ilst, int endPosition) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
|
|
@ -654,7 +643,7 @@ import java.util.List;
|
|||
String s = "" + index;
|
||||
if (count > 0) {
|
||||
s = s + "/" + count;
|
||||
}
|
||||
}
|
||||
Id3Frame frame = new TextInformationFrame(attributeName, s);
|
||||
builder.add(frame);
|
||||
}
|
||||
|
|
@ -665,10 +654,8 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private static void parseStandardGenreAttribute(MetadataBuilder builder,
|
||||
String attributeName,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseStandardGenreAttribute(List<Metadata.Entry> builder,
|
||||
String attributeName, ParsableByteArray ilst) {
|
||||
int length = ilst.readInt() - Atom.FULL_HEADER_SIZE;
|
||||
int key = ilst.readInt();
|
||||
ilst.skipBytes(4);
|
||||
|
|
@ -690,9 +677,8 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private static void parseExtendedAttribute(MetadataBuilder builder,
|
||||
ParsableByteArray ilst,
|
||||
int endPosition) {
|
||||
private static void parseExtendedAttribute(List<Metadata.Entry> builder, ParsableByteArray ilst,
|
||||
int endPosition, GaplessInfoHolder out) {
|
||||
String domain = null;
|
||||
String name = null;
|
||||
Object value = null;
|
||||
|
|
@ -713,9 +699,9 @@ import java.util.List;
|
|||
}
|
||||
|
||||
if (value != null) {
|
||||
if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) {
|
||||
if (!out.hasGaplessInfo() && Util.areEqual(domain, "com.apple.iTunes")) {
|
||||
String s = value instanceof byte[] ? new String((byte[]) value) : value.toString();
|
||||
builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s));
|
||||
out.setFromComment(name, s);
|
||||
}
|
||||
|
||||
if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) {
|
||||
|
|
@ -889,12 +875,12 @@ import java.util.List;
|
|||
/**
|
||||
* Parses a stsd atom (defined in 14496-12).
|
||||
*
|
||||
* @param stsd The stsd atom to decode.
|
||||
* @param trackId The track's identifier in its container.
|
||||
* @param stsd The stsd atom to decode.
|
||||
* @param trackId The track's identifier in its container.
|
||||
* @param rotationDegrees The rotation of the track in degrees.
|
||||
* @param language The language of the track.
|
||||
* @param drmInitData {@link DrmInitData} to be included in the format.
|
||||
* @param isQuickTime True for QuickTime media. False otherwise.
|
||||
* @param language The language of the track.
|
||||
* @param drmInitData {@link DrmInitData} to be included in the format.
|
||||
* @param isQuickTime True for QuickTime media. False otherwise.
|
||||
* @return An object containing the parsed data.
|
||||
*/
|
||||
private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
|
|
@ -311,16 +310,12 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
long durationUs = C.TIME_UNSET;
|
||||
List<Mp4Track> tracks = new ArrayList<>();
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
GaplessInfo gaplessInfo = null;
|
||||
Metadata metadata = null;
|
||||
|
||||
Metadata metadata = null;
|
||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
|
||||
if (udta != null) {
|
||||
Metadata info = AtomParsers.parseUdta(udta, isQuickTime);
|
||||
if (info != null) {
|
||||
gaplessInfo = info.getGaplessInfo();
|
||||
metadata = info;
|
||||
}
|
||||
metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder);
|
||||
}
|
||||
|
||||
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
||||
|
|
@ -337,10 +332,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
|
||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||
gaplessInfoHolder.gaplessInfo = gaplessInfo;
|
||||
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
|
||||
gaplessInfo = gaplessInfoHolder.gaplessInfo;
|
||||
if (trackSampleTable.sampleCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -350,8 +342,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
// Allow ten source samples per output sample, like the platform extractor.
|
||||
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
||||
Format format = track.format.copyWithMaxInputSize(maxInputSize);
|
||||
if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding);
|
||||
if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding);
|
||||
}
|
||||
if (metadata != null) {
|
||||
format = format.copyWithMetadata(metadata);
|
||||
|
|
|
|||
|
|
@ -17,65 +17,79 @@ package com.google.android.exoplayer2.metadata;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ID3 style metadata, with convenient access to gapless playback information.
|
||||
* A collection of metadata entries.
|
||||
*/
|
||||
public class Metadata implements Parcelable {
|
||||
public final class Metadata implements Parcelable {
|
||||
|
||||
private final List<Id3Frame> frames;
|
||||
private final GaplessInfo gaplessInfo;
|
||||
/**
|
||||
* A metadata entry.
|
||||
*/
|
||||
public interface Entry extends Parcelable {}
|
||||
|
||||
public Metadata(List<Id3Frame> frames, GaplessInfo gaplessInfo) {
|
||||
List<Id3Frame> theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList<Id3Frame>();
|
||||
this.frames = Collections.unmodifiableList(theFrames);
|
||||
this.gaplessInfo = gaplessInfo;
|
||||
private final Entry[] entries;
|
||||
|
||||
/**
|
||||
* @param entries The metadata entries.
|
||||
*/
|
||||
public Metadata(Entry... entries) {
|
||||
this.entries = entries == null ? new Entry[0] : entries;
|
||||
}
|
||||
|
||||
public Metadata(Parcel in) {
|
||||
int encoderDelay = in.readInt();
|
||||
int encoderPadding = in.readInt();
|
||||
gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ?
|
||||
new GaplessInfo(encoderDelay, encoderPadding) : null;
|
||||
frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader()));
|
||||
/**
|
||||
* @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];
|
||||
}
|
||||
}
|
||||
|
||||
public Metadata withGaplessInfo(GaplessInfo info) {
|
||||
return new Metadata(frames, info);
|
||||
/* 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());
|
||||
}
|
||||
}
|
||||
|
||||
public List<Id3Frame> getFrames() {
|
||||
return frames;
|
||||
/**
|
||||
* Returns the number of metadata entries.
|
||||
*/
|
||||
public int length() {
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
public GaplessInfo getGaplessInfo() {
|
||||
return gaplessInfo;
|
||||
/**
|
||||
* 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 o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Metadata that = (Metadata) o;
|
||||
|
||||
if (!frames.equals(that.frames)) return false;
|
||||
return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null;
|
||||
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() {
|
||||
int result = frames.hashCode();
|
||||
result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0);
|
||||
return result;
|
||||
return Arrays.hashCode(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -85,21 +99,22 @@ public class Metadata implements Parcelable {
|
|||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1);
|
||||
dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1);
|
||||
dest.writeArray(frames.toArray(new Id3Frame[frames.size()]));
|
||||
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);
|
||||
}
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public Metadata[] newArray(int size) {
|
||||
return new Metadata[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* 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 com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builder for ID3 style metadata.
|
||||
*/
|
||||
public class MetadataBuilder {
|
||||
private List<Id3Frame> frames = new ArrayList<>();
|
||||
private GaplessInfo gaplessInfo;
|
||||
|
||||
public void add(Id3Frame frame) {
|
||||
frames.add(frame);
|
||||
}
|
||||
|
||||
public void setGaplessInfo(GaplessInfo info) {
|
||||
this.gaplessInfo = info;
|
||||
}
|
||||
|
||||
public Metadata build() {
|
||||
return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,8 +15,6 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
|
|
@ -31,7 +29,7 @@ import java.util.Locale;
|
|||
/**
|
||||
* Decodes individual TXXX text frames from raw ID3 data.
|
||||
*/
|
||||
public final class Id3Decoder implements MetadataDecoder<Metadata> {
|
||||
public final class Id3Decoder implements MetadataDecoder {
|
||||
|
||||
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
|
||||
private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
|
||||
|
|
@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
|
|||
private int majorVersion;
|
||||
private int minorVersion;
|
||||
private boolean isUnsynchronized;
|
||||
private GaplessInfo gaplessInfo;
|
||||
|
||||
@Override
|
||||
public boolean canDecode(String mimeType) {
|
||||
|
|
@ -141,11 +138,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
|
|||
frame = decodeTextInformationFrame(frameData, frameSize, id);
|
||||
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
|
||||
(frameId3 == 'M' || frameId3 == 0)) {
|
||||
CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize);
|
||||
frame = commentFrame;
|
||||
if (gaplessInfo == null) {
|
||||
gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text);
|
||||
}
|
||||
frame = decodeCommentFrame(frameData, frameSize);
|
||||
} else {
|
||||
String id = frameId3 != 0 ?
|
||||
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
|
||||
|
|
@ -159,7 +152,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
|
|||
}
|
||||
}
|
||||
|
||||
return new Metadata(id3Frames, null);
|
||||
return new Metadata(id3Frames);
|
||||
}
|
||||
|
||||
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
|
||||
|
|
@ -198,7 +191,7 @@ public final class Id3Decoder implements MetadataDecoder<Metadata> {
|
|||
/**
|
||||
* @param id3Buffer A {@link ParsableByteArray} from which data should be read.
|
||||
* @return The size of ID3 frames in bytes, excluding the header and footer.
|
||||
* @throws ParserException If ID3 file identifier != "ID3".
|
||||
* @throws MetadataDecoderException If ID3 file identifier != "ID3".
|
||||
*/
|
||||
private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException {
|
||||
int id1 = id3Buffer.readUnsignedByte();
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@
|
|||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
* Base class for ID3 frames.
|
||||
*/
|
||||
public abstract class Id3Frame implements Parcelable {
|
||||
public abstract class Id3Frame implements Metadata.Entry {
|
||||
|
||||
/**
|
||||
* The frame ID.
|
||||
|
|
|
|||
Loading…
Reference in a new issue