Skip to content

Commit

Permalink
Support seeking based on MLLT metadata
Browse files Browse the repository at this point in the history
Issue: #3241

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=217252254
  • Loading branch information
andrewlewis authored and ojw28 committed Oct 18, 2018
1 parent bfd6799 commit ee02c67
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 15 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
([#4788](https://github.com/google/ExoPlayer/issues/4788)).
* SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Audio:
* Support seeking based on MLLT metadata
([#3241](https://github.com/google/ExoPlayer/issues/3241)).
* Fix issue where buffered position is not updated correctly when transitioning
between periods
([#4899](https://github.com/google/ExoPlayer/issues/4899)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -28,15 +27,6 @@
*/
public final class GaplessInfoHolder {

/**
* A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed to
* {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback information
* are decoded.
*/
public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE =
(majorVersion, id0, id1, id2, id3) ->
id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2);

private static final String GAPLESS_DOMAIN = "com.apple.iTunes";
private static final String GAPLESS_DESCRIPTION = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2018 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.mp3;

import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.metadata.id3.MlltFrame;
import com.google.android.exoplayer2.util.Util;

/** MP3 seeker that uses metadata from an {@link MlltFrame}. */
/* package */ final class MlltSeeker implements Mp3Extractor.Seeker {

/**
* Returns an {@link MlltSeeker} for seeking in the stream.
*
* @param firstFramePosition The position of the start of the first frame in the stream.
* @param mlltFrame The MLLT frame with seeking metadata.
* @return An {@link MlltSeeker} for seeking in the stream.
*/
public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) {
int referenceCount = mlltFrame.bytesDeviations.length;
long[] referencePositions = new long[1 + referenceCount];
long[] referenceTimesMs = new long[1 + referenceCount];
referencePositions[0] = firstFramePosition;
referenceTimesMs[0] = 0;
long position = firstFramePosition;
long timeMs = 0;
for (int i = 1; i <= referenceCount; i++) {
position += mlltFrame.bytesBetweenReference + mlltFrame.bytesDeviations[i - 1];
timeMs += mlltFrame.millisecondsBetweenReference + mlltFrame.millisecondsDeviations[i - 1];
referencePositions[i] = position;
referenceTimesMs[i] = timeMs;
}
return new MlltSeeker(referencePositions, referenceTimesMs);
}

private final long[] referencePositions;
private final long[] referenceTimesMs;
private final long durationUs;

private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) {
this.referencePositions = referencePositions;
this.referenceTimesMs = referenceTimesMs;
// Use the last reference point as the duration, as extrapolating variable bitrate at the end of
// the stream may give a large error.
durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);
}

@Override
public boolean isSeekable() {
return true;
}

@Override
public SeekPoints getSeekPoints(long timeUs) {
timeUs = Util.constrainValue(timeUs, 0, durationUs);
Pair<Long, Long> timeMsAndPosition =
linearlyInterpolate(C.usToMs(timeUs), referenceTimesMs, referencePositions);
timeUs = C.msToUs(timeMsAndPosition.first);
long position = timeMsAndPosition.second;
return new SeekPoints(new SeekPoint(timeUs, position));
}

@Override
public long getTimeUs(long position) {
Pair<Long, Long> positionAndTimeMs =
linearlyInterpolate(position, referencePositions, referenceTimesMs);
return C.msToUs(positionAndTimeMs.second);
}

@Override
public long getDurationUs() {
return durationUs;
}

/**
* Given a set of reference points as coordinates in {@code xReferences} and {@code yReferences}
* and an x-axis value, linearly interpolates between corresponding reference points to give a
* y-axis value.
*
* @param x The x-axis value for which a y-axis value is needed.
* @param xReferences x coordinates of reference points.
* @param yReferences y coordinates of reference points.
* @return The linearly interpolated y-axis value.
*/
private static Pair<Long, Long> linearlyInterpolate(
long x, long[] xReferences, long[] yReferences) {
int previousReferenceIndex =
Util.binarySearchFloor(xReferences, x, /* inclusive= */ true, /* stayInBounds= */ true);
long xPreviousReference = xReferences[previousReferenceIndex];
long yPreviousReference = yReferences[previousReferenceIndex];
int nextReferenceIndex = previousReferenceIndex + 1;
if (nextReferenceIndex == xReferences.length) {
return Pair.create(xPreviousReference, yPreviousReference);
} else {
long xNextReference = xReferences[nextReferenceIndex];
long yNextReference = yReferences[nextReferenceIndex];
double proportion =
xNextReference == xPreviousReference
? 0.0
: ((double) x - xPreviousReference) / (xNextReference - xPreviousReference);
long y = (long) (proportion * (yNextReference - yPreviousReference)) + yPreviousReference;
return Pair.create(x, y);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3;

import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
Expand All @@ -31,6 +32,8 @@
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.metadata.id3.Id3Decoder.FramePredicate;
import com.google.android.exoplayer2.metadata.id3.MlltFrame;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
Expand Down Expand Up @@ -68,6 +71,12 @@ public final class Mp3Extractor implements Extractor {
*/
public static final int FLAG_DISABLE_ID3_METADATA = 2;

/** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */
private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE =
(majorVersion, id0, id1, id2, id3) ->
((id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2))
|| (id0 == 'M' && id1 == 'L' && id2 == 'L' && (id3 == 'T' || majorVersion == 2)));

/**
* The maximum number of bytes to search when synchronizing, before giving up.
*/
Expand Down Expand Up @@ -174,7 +183,15 @@ public int read(ExtractorInput input, PositionHolder seekPosition)
}
}
if (seeker == null) {
seeker = maybeReadSeekFrame(input);
// Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata
// takes priority as it can provide greater precision.
Seeker seekFrameSeeker = maybeReadSeekFrame(input);
Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition());
if (metadataSeeker != null) {
seeker = metadataSeeker;
} else if (seekFrameSeeker != null) {
seeker = seekFrameSeeker;
}
if (seeker == null
|| (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
seeker = getConstantBitrateSeeker(input);
Expand Down Expand Up @@ -253,11 +270,11 @@ private boolean synchronize(ExtractorInput input, boolean sniffing)
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition();
if (input.getPosition() == 0) {
// We need to parse enough ID3 metadata to retrieve any gapless playback information even
// if ID3 metadata parsing is disabled.
boolean onlyDecodeGaplessInfoFrames = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
// We need to parse enough ID3 metadata to retrieve any gapless/seeking playback information
// even if ID3 metadata parsing is disabled.
boolean parseAllId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) == 0;
Id3Decoder.FramePredicate id3FramePredicate =
onlyDecodeGaplessInfoFrames ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
parseAllId3Frames ? null : REQUIRED_ID3_FRAME_PREDICATE;
metadata = id3Peeker.peekId3Data(input, id3FramePredicate);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
Expand Down Expand Up @@ -401,6 +418,20 @@ private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) {
return SEEK_HEADER_UNSET;
}

@Nullable
private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) {
if (metadata != null) {
int length = metadata.length();
for (int i = 0; i < length; i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof MlltFrame) {
return MlltSeeker.create(firstFramePosition, (MlltFrame) entry);
}
}
}
return null;
}

/**
* {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
* used to work out the new sample basis timestamp after seeking and resynchronization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.UnsupportedEncodingException;
Expand Down Expand Up @@ -382,6 +383,8 @@ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersio
} else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
frameHeaderSize, framePredicate);
} else if (frameId0 == 'M' && frameId1 == 'L' && frameId2 == 'L' && frameId3 == 'T') {
frame = decodeMlltFrame(id3Data, frameSize);
} else {
String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
frame = decodeBinaryFrame(id3Data, frameSize, id);
Expand Down Expand Up @@ -662,6 +665,36 @@ private static ChapterTocFrame decodeChapterTOCFrame(
return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray);
}

private static MlltFrame decodeMlltFrame(ParsableByteArray id3Data, int frameSize) {
// See ID3v2.4.0 native frames subsection 4.6.
int mpegFramesBetweenReference = id3Data.readUnsignedShort();
int bytesBetweenReference = id3Data.readUnsignedInt24();
int millisecondsBetweenReference = id3Data.readUnsignedInt24();
int bitsForBytesDeviation = id3Data.readUnsignedByte();
int bitsForMillisecondsDeviation = id3Data.readUnsignedByte();

ParsableBitArray references = new ParsableBitArray();
references.reset(id3Data);
int referencesBits = 8 * (frameSize - 10);
int bitsPerReference = bitsForBytesDeviation + bitsForMillisecondsDeviation;
int referencesCount = referencesBits / bitsPerReference;
int[] bytesDeviations = new int[referencesCount];
int[] millisecondsDeviations = new int[referencesCount];
for (int i = 0; i < referencesCount; i++) {
int bytesDeviation = references.readBits(bitsForBytesDeviation);
int millisecondsDeviation = references.readBits(bitsForMillisecondsDeviation);
bytesDeviations[i] = bytesDeviation;
millisecondsDeviations[i] = millisecondsDeviation;
}

return new MlltFrame(
mpegFramesBetweenReference,
bytesBetweenReference,
millisecondsBetweenReference,
bytesDeviations,
millisecondsDeviations);
}

private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
String id) {
byte[] frame = new byte[frameSize];
Expand Down
Loading

0 comments on commit ee02c67

Please sign in to comment.