diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce17e878928..aac69e14380 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes # -### 2.10.6 (2019-10-17) ### +### 2.10.6 (2019-10-18) ### * Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to detect playbacks suppressions (e.g. transient audio focus loss) directly @@ -12,6 +12,10 @@ ([#6523](https://github.com/google/ExoPlayer/issues/6523)). * HLS: Add support for ID3 in EMSG when using FMP4 streams ([spec](https://aomediacodec.github.io/av1-id3/)). +* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast + live streams ([#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)). * Metadata: Expose the raw ICY metadata through `IcyInfo` ([#6476](https://github.com/google/ExoPlayer/issues/6476)). * UI: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 0cb55dffa50..15a98ab5add 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -25,7 +25,7 @@ public interface SeekMap { /** A {@link SeekMap} that does not support seeking. */ - final class Unseekable implements SeekMap { + class Unseekable implements SeekMap { private final long durationUs; private final SeekPoints startSeekPoints; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index f4007207728..4a5feb5096b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -22,8 +22,7 @@ /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. */ -/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap - implements Mp3Extractor.Seeker { +/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker { /** * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java index 868c1d9fbff..1b627483f08 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.util.Util; /** 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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index bc218e26ad6..6134f042c29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -28,8 +28,8 @@ import com.google.android.exoplayer2.extractor.Id3Peeker; 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.extractor.mp3.Seeker.UnseekableSeeker; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; @@ -114,7 +114,8 @@ public final class Mp3Extractor implements Extractor { private int synchronizedHeaderData; private Metadata metadata; - private Seeker seeker; + @Nullable private Seeker seeker; + private boolean disableSeeking; private long basisTimeUs; private long samplesRead; private long firstSamplePosition; @@ -188,14 +189,19 @@ public int read(ExtractorInput input, PositionHolder seekPosition) // 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); + + if (disableSeeking) { + seeker = new UnseekableSeeker(); + } else { + 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); + } } extractorOutput.seekMap(seeker); trackOutput.format( @@ -226,6 +232,15 @@ public int read(ExtractorInput input, PositionHolder seekPosition) return readSample(input); } + /** + * Disables the extractor from being able to seek through the media. + * + *

Please note that this needs to be called before {@link #read}. + */ + public void disableSeeking() { + disableSeeking = true; + } + // Internal methods. private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { @@ -464,26 +479,5 @@ private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstF 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(); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java new file mode 100644 index 00000000000..c5b7550f2d8 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java @@ -0,0 +1,60 @@ +/* + * 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; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index ba8b26b7c19..86551319e10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -23,10 +23,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -/** - * MP3 seeker that uses metadata from a VBRI header. - */ -/* package */ final class VbriSeeker implements Mp3Extractor.Seeker { +/** MP3 seeker that uses metadata from a VBRI header. */ +/* package */ final class VbriSeeker implements Seeker { private static final String TAG = "VbriSeeker"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 116a1230944..7094f327c82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -24,10 +24,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -/** - * MP3 seeker that uses metadata from a Xing header. - */ -/* package */ final class XingSeeker implements Mp3Extractor.Seeker { +/** MP3 seeker that uses metadata from a Xing header. */ +/* package */ final class XingSeeker implements Seeker { private static final String TAG = "XingSeeker"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index a5a3efa2876..f7f8a4506b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.SeekMap.Unseekable; 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.icy.IcyHeaders; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -949,6 +950,12 @@ public void load() throws IOException, InterruptedException { } input = new DefaultExtractorInput(extractorDataSource, position, length); 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) { extractor.seek(position, seekTimeUs); pendingExtractorSeek = false;