mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Added an ICY header workaround for unseekable MP3 streams.
Issue:#6537 PiperOrigin-RevId: 275477266
This commit is contained in:
parent
b7f335c715
commit
694ccf424b
9 changed files with 104 additions and 43 deletions
|
|
@ -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;
|
||||||
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;
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
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;
|
||||||
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 {
|
||||||
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,14 +188,19 @@ 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 (metadataSeeker != null) {
|
|
||||||
seeker = metadataSeeker;
|
if (disableSeeking) {
|
||||||
} else if (seekFrameSeeker != null) {
|
seeker = new UnseekableSeeker();
|
||||||
seeker = seekFrameSeeker;
|
} else {
|
||||||
}
|
if (metadataSeeker != null) {
|
||||||
if (seeker == null
|
seeker = metadataSeeker;
|
||||||
|| (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
|
} else if (seekFrameSeeker != null) {
|
||||||
seeker = getConstantBitrateSeeker(input);
|
seeker = seekFrameSeeker;
|
||||||
|
}
|
||||||
|
if (seeker == null
|
||||||
|
|| (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
|
||||||
|
seeker = getConstantBitrateSeeker(input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
extractorOutput.seekMap(seeker);
|
extractorOutput.seekMap(seeker);
|
||||||
trackOutput.format(
|
trackOutput.format(
|
||||||
|
|
@ -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 {
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue