Commit 694ccf42 by samrobinson Committed by Oliver Woodman

Added an ICY header workaround for unseekable MP3 streams.

Issue:#6537
PiperOrigin-RevId: 275477266
parent b7f335c7
...@@ -93,6 +93,11 @@ ...@@ -93,6 +93,11 @@
fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)).
* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state.
* Make show and hide player controls accessible for TalkBack in `PlayerView`. * Make show and hide player controls accessible for TalkBack in `PlayerView`.
* Add workaround to avoid truncating MP3 live streams with ICY metadata and
introductions that have a seeking header
([#6537](https://github.com/google/ExoPlayer/issues/6537),
[#6315](https://github.com/google/ExoPlayer/issues/6315) and
[#5658](https://github.com/google/ExoPlayer/issues/5658)).
* Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`.
### 2.10.6 (2019-10-17) ### ### 2.10.6 (2019-10-17) ###
......
...@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Assertions;
public interface SeekMap { public interface SeekMap {
/** A {@link SeekMap} that does not support seeking. */ /** A {@link SeekMap} that does not support seeking. */
final class Unseekable implements SeekMap { class Unseekable implements SeekMap {
private final long durationUs; private final long durationUs;
private final SeekPoints startSeekPoints; private final SeekPoints startSeekPoints;
......
...@@ -22,8 +22,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader; ...@@ -22,8 +22,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader;
/** /**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
*/ */
/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap /* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker {
implements Mp3Extractor.Seeker {
/** /**
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
......
...@@ -22,7 +22,7 @@ import com.google.android.exoplayer2.metadata.id3.MlltFrame; ...@@ -22,7 +22,7 @@ import com.google.android.exoplayer2.metadata.id3.MlltFrame;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** MP3 seeker that uses metadata from an {@link MlltFrame}. */ /** MP3 seeker that uses metadata from an {@link MlltFrame}. */
/* package */ final class MlltSeeker implements Mp3Extractor.Seeker { /* package */ final class MlltSeeker implements Seeker {
/** /**
* Returns an {@link MlltSeeker} for seeking in the stream. * Returns an {@link MlltSeeker} for seeking in the stream.
......
...@@ -28,8 +28,8 @@ import com.google.android.exoplayer2.extractor.GaplessInfoHolder; ...@@ -28,8 +28,8 @@ import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.Id3Peeker;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder; 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.TrackOutput;
import com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
...@@ -113,7 +113,8 @@ public final class Mp3Extractor implements Extractor { ...@@ -113,7 +113,8 @@ public final class Mp3Extractor implements Extractor {
private int synchronizedHeaderData; private int synchronizedHeaderData;
private Metadata metadata; private Metadata metadata;
private Seeker seeker; @Nullable private Seeker seeker;
private boolean disableSeeking;
private long basisTimeUs; private long basisTimeUs;
private long samplesRead; private long samplesRead;
private long firstSamplePosition; private long firstSamplePosition;
...@@ -187,6 +188,10 @@ public final class Mp3Extractor implements Extractor { ...@@ -187,6 +188,10 @@ public final class Mp3Extractor implements Extractor {
// takes priority as it can provide greater precision. // takes priority as it can provide greater precision.
Seeker seekFrameSeeker = maybeReadSeekFrame(input); Seeker seekFrameSeeker = maybeReadSeekFrame(input);
Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition());
if (disableSeeking) {
seeker = new UnseekableSeeker();
} else {
if (metadataSeeker != null) { if (metadataSeeker != null) {
seeker = metadataSeeker; seeker = metadataSeeker;
} else if (seekFrameSeeker != null) { } else if (seekFrameSeeker != null) {
...@@ -196,6 +201,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -196,6 +201,7 @@ public final class Mp3Extractor implements Extractor {
|| (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
seeker = getConstantBitrateSeeker(input); seeker = getConstantBitrateSeeker(input);
} }
}
extractorOutput.seekMap(seeker); extractorOutput.seekMap(seeker);
trackOutput.format( trackOutput.format(
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
...@@ -225,6 +231,15 @@ public final class Mp3Extractor implements Extractor { ...@@ -225,6 +231,15 @@ public final class Mp3Extractor implements Extractor {
return readSample(input); return readSample(input);
} }
/**
* Disables the extractor from being able to seek through the media.
*
* <p>Please note that this needs to be called before {@link #read}.
*/
public void disableSeeking() {
disableSeeking = true;
}
// Internal methods. // Internal methods.
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
...@@ -463,26 +478,5 @@ public final class Mp3Extractor implements Extractor { ...@@ -463,26 +478,5 @@ public final class Mp3Extractor implements Extractor {
return null; return null;
} }
/**
* {@link SeekMap} that provides the end position of audio data and 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.
*/
/* package */ interface Seeker extends SeekMap {
/**
* Maps a position (byte offset) to a corresponding sample timestamp.
*
* @param position A seek position (byte offset) relative to the start of the stream.
* @return The corresponding timestamp of the next sample to be read, in microseconds.
*/
long getTimeUs(long position);
/**
* Returns the position (byte offset) in the stream that is immediately after audio data, or
* {@link C#POSITION_UNSET} if not known.
*/
long getDataEndPosition();
}
} }
/*
* Copyright (C) 2019 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 com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekMap;
/**
* {@link SeekMap} that provides the end position of audio data and 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.
*/
/* package */ interface Seeker extends SeekMap {
/**
* Maps a position (byte offset) to a corresponding sample timestamp.
*
* @param position A seek position (byte offset) relative to the start of the stream.
* @return The corresponding timestamp of the next sample to be read, in microseconds.
*/
long getTimeUs(long position);
/**
* Returns the position (byte offset) in the stream that is immediately after audio data, or
* {@link C#POSITION_UNSET} if not known.
*/
long getDataEndPosition();
/** A {@link Seeker} that does not support seeking through audio data. */
/* package */ class UnseekableSeeker extends SeekMap.Unseekable implements Seeker {
public UnseekableSeeker() {
super(/* durationUs= */ C.TIME_UNSET);
}
@Override
public long getTimeUs(long position) {
return 0;
}
@Override
public long getDataEndPosition() {
// Position unset as we do not know the data end position. Note that returning 0 doesn't work.
return C.POSITION_UNSET;
}
}
}
...@@ -23,10 +23,8 @@ import com.google.android.exoplayer2.util.Log; ...@@ -23,10 +23,8 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /** MP3 seeker that uses metadata from a VBRI header. */
* MP3 seeker that uses metadata from a VBRI header. /* package */ final class VbriSeeker implements Seeker {
*/
/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "VbriSeeker"; private static final String TAG = "VbriSeeker";
......
...@@ -24,10 +24,8 @@ import com.google.android.exoplayer2.util.Log; ...@@ -24,10 +24,8 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /** MP3 seeker that uses metadata from a Xing header. */
* MP3 seeker that uses metadata from a Xing header. /* package */ final class XingSeeker implements Seeker {
*/
/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "XingSeeker"; private static final String TAG = "XingSeeker";
......
...@@ -34,6 +34,7 @@ import com.google.android.exoplayer2.extractor.SeekMap; ...@@ -34,6 +34,7 @@ import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
import com.google.android.exoplayer2.extractor.SeekMap.Unseekable; import com.google.android.exoplayer2.extractor.SeekMap.Unseekable;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
...@@ -985,6 +986,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -985,6 +986,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
input = new DefaultExtractorInput(extractorDataSource, position, length); input = new DefaultExtractorInput(extractorDataSource, position, length);
Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri); Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri);
// MP3 live streams commonly have seekable metadata, despite being unseekable.
if (icyHeaders != null && extractor instanceof Mp3Extractor) {
((Mp3Extractor) extractor).disableSeeking();
}
if (pendingExtractorSeek) { if (pendingExtractorSeek) {
extractor.seek(position, seekTimeUs); extractor.seek(position, seekTimeUs);
pendingExtractorSeek = false; pendingExtractorSeek = false;
......
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