mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
commit
d2e480f01b
@ -68,10 +68,10 @@ import android.widget.TextView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
maybeFail();
|
||||
if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) {
|
||||
currentPositionUs = timeUs;
|
||||
if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) {
|
||||
currentPositionUs = positionUs;
|
||||
textView.post(this);
|
||||
}
|
||||
}
|
||||
|
@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl {
|
||||
// Update the loader state.
|
||||
int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs);
|
||||
LoaderState loaderState = loaderStates.get(loader);
|
||||
boolean loaderStateChanged = loaderState.bufferState != loaderBufferState ||
|
||||
loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading ||
|
||||
loaderState.failed != failed;
|
||||
boolean loaderStateChanged = loaderState.bufferState != loaderBufferState
|
||||
|| loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading
|
||||
|| loaderState.failed != failed;
|
||||
if (loaderStateChanged) {
|
||||
loaderState.bufferState = loaderBufferState;
|
||||
loaderState.nextLoadPositionUs = nextLoadPositionUs;
|
||||
@ -214,17 +214,17 @@ public class DefaultLoadControl implements LoadControl {
|
||||
private void updateControlState() {
|
||||
boolean loading = false;
|
||||
boolean failed = false;
|
||||
boolean finished = true;
|
||||
boolean haveNextLoadPosition = false;
|
||||
int highestState = bufferPoolState;
|
||||
for (int i = 0; i < loaders.size(); i++) {
|
||||
LoaderState loaderState = loaderStates.get(loaders.get(i));
|
||||
loading |= loaderState.loading;
|
||||
failed |= loaderState.failed;
|
||||
finished &= loaderState.nextLoadPositionUs == -1;
|
||||
haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1;
|
||||
highestState = Math.max(highestState, loaderState.bufferState);
|
||||
}
|
||||
|
||||
fillingBuffers = !loaders.isEmpty() && !finished && !failed
|
||||
fillingBuffers = !loaders.isEmpty() && !failed && (loading || haveNextLoadPosition)
|
||||
&& (highestState == BELOW_LOW_WATERMARK
|
||||
|| (highestState == BETWEEN_WATERMARKS && fillingBuffers));
|
||||
if (fillingBuffers && !streamingPrioritySet) {
|
||||
|
@ -40,12 +40,12 @@ public class DummyTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) {
|
||||
protected void seekTo(long positionUs) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) {
|
||||
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,7 @@ import java.util.List;
|
||||
private int state;
|
||||
private int customMessagesSent = 0;
|
||||
private int customMessagesProcessed = 0;
|
||||
private long elapsedRealtimeUs;
|
||||
|
||||
private volatile long durationUs;
|
||||
private volatile long positionUs;
|
||||
@ -383,7 +384,8 @@ import java.util.List;
|
||||
positionUs = timeSourceTrackRenderer != null &&
|
||||
enabledRenderers.contains(timeSourceTrackRenderer) ?
|
||||
timeSourceTrackRenderer.getCurrentPositionUs() :
|
||||
mediaClock.getTimeUs();
|
||||
mediaClock.getPositionUs();
|
||||
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
}
|
||||
|
||||
private void doSomeWork() throws ExoPlaybackException {
|
||||
@ -399,7 +401,7 @@ import java.util.List;
|
||||
// TODO: Each renderer should return the maximum delay before which it wishes to be
|
||||
// invoked again. The minimum of these values should then be used as the delay before the next
|
||||
// invocation of this method.
|
||||
renderer.doSomeWork(positionUs);
|
||||
renderer.doSomeWork(positionUs, elapsedRealtimeUs);
|
||||
isEnded = isEnded && renderer.isEnded();
|
||||
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
||||
|
||||
@ -462,7 +464,7 @@ import java.util.List;
|
||||
rebuffering = false;
|
||||
positionUs = positionMs * 1000L;
|
||||
mediaClock.stop();
|
||||
mediaClock.setTimeUs(positionUs);
|
||||
mediaClock.setPositionUs(positionUs);
|
||||
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
|
||||
return;
|
||||
}
|
||||
@ -582,7 +584,7 @@ import java.util.List;
|
||||
if (renderer == timeSourceTrackRenderer) {
|
||||
// We've been using timeSourceTrackRenderer to advance the current position, but it's
|
||||
// being disabled. Sync mediaClock so that it can take over timing responsibilities.
|
||||
mediaClock.setTimeUs(renderer.getCurrentPositionUs());
|
||||
mediaClock.setPositionUs(renderer.getCurrentPositionUs());
|
||||
}
|
||||
ensureStopped(renderer);
|
||||
enabledRenderers.remove(renderer);
|
||||
|
@ -50,7 +50,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||
private int[] trackStates;
|
||||
private boolean[] pendingDiscontinuities;
|
||||
|
||||
private long seekTimeUs;
|
||||
private long seekPositionUs;
|
||||
|
||||
public FrameworkSampleSource(Context context, Uri uri, Map<String, String> headers,
|
||||
int downstreamRendererCount) {
|
||||
@ -94,16 +94,16 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(int track, long timeUs) {
|
||||
public void enable(int track, long positionUs) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
|
||||
trackStates[track] = TRACK_STATE_ENABLED;
|
||||
extractor.selectTrack(track);
|
||||
seekToUs(timeUs);
|
||||
seekToUs(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueBuffering(long playbackPositionUs) {
|
||||
public boolean continueBuffering(long positionUs) {
|
||||
// MediaExtractor takes care of buffering and blocks until it has samples, so we can always
|
||||
// return true here. Although note that the blocking behavior is itself as bug, as per the
|
||||
// TODO further up this file. This method will need to return something else as part of fixing
|
||||
@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
|
||||
@ -144,7 +144,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
|
||||
sampleHolder.cryptoInfo.setFromExtractorV16(extractor);
|
||||
}
|
||||
seekTimeUs = -1;
|
||||
seekPositionUs = -1;
|
||||
extractor.advance();
|
||||
return SAMPLE_READ;
|
||||
} else {
|
||||
@ -168,13 +168,13 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToUs(long timeUs) {
|
||||
public void seekToUs(long positionUs) {
|
||||
Assertions.checkState(prepared);
|
||||
if (seekTimeUs != timeUs) {
|
||||
if (seekPositionUs != positionUs) {
|
||||
// Avoid duplicate calls to the underlying extractor's seek method in the case that there
|
||||
// have been no interleaving calls to advance.
|
||||
seekTimeUs = timeUs;
|
||||
extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
seekPositionUs = positionUs;
|
||||
extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
for (int i = 0; i < trackStates.length; ++i) {
|
||||
if (trackStates[i] != TRACK_STATE_DISABLED) {
|
||||
pendingDiscontinuities[i] = true;
|
||||
|
@ -65,9 +65,10 @@ public interface LoadControl {
|
||||
*
|
||||
* @param loader The loader invoking the update.
|
||||
* @param playbackPositionUs The loader's playback position.
|
||||
* @param nextLoadPositionUs The loader's next load position, or -1 if finished.
|
||||
* @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the
|
||||
* next load position is not yet known.
|
||||
* @param loading Whether the loader is currently loading data.
|
||||
* @param failed Whether the loader has failed, meaning it does not wish to load more data.
|
||||
* @param failed Whether the loader has failed.
|
||||
* @return True if the loader is allowed to start its next load. False otherwise.
|
||||
*/
|
||||
boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
|
||||
|
@ -29,10 +29,10 @@ import android.os.SystemClock;
|
||||
/**
|
||||
* The media time when the clock was last set or stopped.
|
||||
*/
|
||||
private long timeUs;
|
||||
private long positionUs;
|
||||
|
||||
/**
|
||||
* The difference between {@link SystemClock#elapsedRealtime()} and {@link #timeUs}
|
||||
* The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs}
|
||||
* when the clock was last set or started.
|
||||
*/
|
||||
private long deltaUs;
|
||||
@ -43,7 +43,7 @@ import android.os.SystemClock;
|
||||
public void start() {
|
||||
if (!started) {
|
||||
started = true;
|
||||
deltaUs = elapsedRealtimeMinus(timeUs);
|
||||
deltaUs = elapsedRealtimeMinus(positionUs);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,28 +52,28 @@ import android.os.SystemClock;
|
||||
*/
|
||||
public void stop() {
|
||||
if (started) {
|
||||
timeUs = elapsedRealtimeMinus(deltaUs);
|
||||
positionUs = elapsedRealtimeMinus(deltaUs);
|
||||
started = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timeUs The time to set in microseconds.
|
||||
* @param timeUs The position to set in microseconds.
|
||||
*/
|
||||
public void setTimeUs(long timeUs) {
|
||||
this.timeUs = timeUs;
|
||||
public void setPositionUs(long timeUs) {
|
||||
this.positionUs = timeUs;
|
||||
deltaUs = elapsedRealtimeMinus(timeUs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current time in microseconds.
|
||||
* @return The current position in microseconds.
|
||||
*/
|
||||
public long getTimeUs() {
|
||||
return started ? elapsedRealtimeMinus(deltaUs) : timeUs;
|
||||
public long getPositionUs() {
|
||||
return started ? elapsedRealtimeMinus(deltaUs) : positionUs;
|
||||
}
|
||||
|
||||
private long elapsedRealtimeMinus(long microSeconds) {
|
||||
return SystemClock.elapsedRealtime() * 1000 - microSeconds;
|
||||
private long elapsedRealtimeMinus(long toSubtractUs) {
|
||||
return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -94,10 +94,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
/**
|
||||
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
|
||||
* than this amount. This is a fail safe that should not be required on correctly functioning
|
||||
* devices.
|
||||
* than this amount.
|
||||
* <p>
|
||||
* This is a fail safe that should not be required on correctly functioning devices.
|
||||
*/
|
||||
private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND;
|
||||
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND;
|
||||
|
||||
/**
|
||||
* AudioTrack latencies are deemed impossibly large if they are greater than this amount.
|
||||
* <p>
|
||||
* This is a fail safe that should not be required on correctly functioning devices.
|
||||
*/
|
||||
private static final long MAX_AUDIO_TRACK_LATENCY_US = 10 * MICROS_PER_SECOND;
|
||||
|
||||
private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
|
||||
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
|
||||
@ -261,14 +269,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long timeUs, boolean joining) {
|
||||
super.onEnabled(timeUs, joining);
|
||||
protected void onEnabled(long positionUs, boolean joining) {
|
||||
super.onEnabled(positionUs, joining);
|
||||
lastReportedCurrentPositionUs = Long.MIN_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
super.doSomeWork(timeUs);
|
||||
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
super.doSomeWork(positionUs, elapsedRealtimeUs);
|
||||
maybeSampleSyncParams();
|
||||
}
|
||||
|
||||
@ -515,7 +523,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
if (audioTimestampUs < audioTrackResumeSystemTimeUs) {
|
||||
// The timestamp corresponds to a time before the track was most recently resumed.
|
||||
audioTimestampSet = false;
|
||||
} else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) {
|
||||
} else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
|
||||
// The timestamp time base is probably wrong.
|
||||
audioTimestampSet = false;
|
||||
Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", "
|
||||
@ -531,6 +539,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
framesToDurationUs(bufferSize / frameSize);
|
||||
// Sanity check that the latency is non-negative.
|
||||
audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0);
|
||||
// Sanity check that the latency isn't too large.
|
||||
if (audioTrackLatencyUs > MAX_AUDIO_TRACK_LATENCY_US) {
|
||||
Log.w(TAG, "Ignoring impossibly large audio latency: " + audioTrackLatencyUs);
|
||||
audioTrackLatencyUs = 0;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// The method existed, but doesn't work. Don't try again.
|
||||
audioTrackGetLatencyMethod = null;
|
||||
@ -572,16 +585,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
super.seekTo(timeUs);
|
||||
protected void seekTo(long positionUs) throws ExoPlaybackException {
|
||||
super.seekTo(positionUs);
|
||||
// TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed.
|
||||
releaseAudioTrack();
|
||||
lastReportedCurrentPositionUs = Long.MIN_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
|
||||
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
|
||||
ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
|
||||
throws ExoPlaybackException {
|
||||
if (shouldSkip) {
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
@ -616,6 +629,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
// time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to
|
||||
// allow time to jump backwards if it really wants to.
|
||||
audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
|
||||
audioTrackStartMediaTimeState = START_IN_SYNC;
|
||||
lastReportedCurrentPositionUs = Long.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
@ -217,13 +217,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long timeUs, boolean joining) {
|
||||
source.enable(trackIndex, timeUs);
|
||||
protected void onEnabled(long positionUs, boolean joining) {
|
||||
source.enable(trackIndex, positionUs);
|
||||
sourceState = SOURCE_STATE_NOT_READY;
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
waitingForKeys = false;
|
||||
currentPositionUs = timeUs;
|
||||
currentPositionUs = positionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -367,9 +367,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
currentPositionUs = timeUs;
|
||||
source.seekToUs(timeUs);
|
||||
protected void seekTo(long positionUs) throws ExoPlaybackException {
|
||||
currentPositionUs = positionUs;
|
||||
source.seekToUs(positionUs);
|
||||
sourceState = SOURCE_STATE_NOT_READY;
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
@ -387,22 +387,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
try {
|
||||
sourceState = source.continueBuffering(timeUs)
|
||||
sourceState = source.continueBuffering(positionUs)
|
||||
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
|
||||
: SOURCE_STATE_NOT_READY;
|
||||
checkForDiscontinuity();
|
||||
if (format == null) {
|
||||
readFormat();
|
||||
} else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) {
|
||||
discardSamples(timeUs);
|
||||
discardSamples(positionUs);
|
||||
} else {
|
||||
if (codec == null && shouldInitCodec()) {
|
||||
maybeInitCodec();
|
||||
}
|
||||
if (codec != null) {
|
||||
while (drainOutputBuffer(timeUs)) {}
|
||||
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
|
||||
if (feedInputBuffer(true)) {
|
||||
while (feedInputBuffer(false)) {}
|
||||
}
|
||||
@ -421,10 +421,10 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private void discardSamples(long timeUs) throws IOException, ExoPlaybackException {
|
||||
private void discardSamples(long positionUs) throws IOException, ExoPlaybackException {
|
||||
sampleHolder.data = null;
|
||||
int result = SampleSource.SAMPLE_READ;
|
||||
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
|
||||
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= positionUs) {
|
||||
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
if (!sampleHolder.decodeOnly) {
|
||||
@ -469,7 +469,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
|
||||
/**
|
||||
* @param firstFeed True if this is the first call to this method from the current invocation of
|
||||
* {@link #doSomeWork(long)}. False otherwise.
|
||||
* {@link #doSomeWork(long, long)}. False otherwise.
|
||||
* @return True if it may be possible to feed more input data. False otherwise.
|
||||
* @throws IOException If an error occurs reading data from the upstream source.
|
||||
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
||||
@ -694,7 +694,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
* @return True if it may be possible to drain more output data. False otherwise.
|
||||
* @throws ExoPlaybackException If an error occurs draining the output buffer.
|
||||
*/
|
||||
private boolean drainOutputBuffer(long timeUs) throws ExoPlaybackException {
|
||||
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
||||
throws ExoPlaybackException {
|
||||
if (outputStreamEnded) {
|
||||
return false;
|
||||
}
|
||||
@ -722,8 +723,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
|
||||
boolean decodeOnly = decodeOnlyPresentationTimestamps.contains(
|
||||
outputBufferInfo.presentationTimeUs);
|
||||
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
|
||||
outputIndex, decodeOnly)) {
|
||||
if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
|
||||
outputBufferInfo, outputIndex, decodeOnly)) {
|
||||
if (decodeOnly) {
|
||||
decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs);
|
||||
} else {
|
||||
@ -743,9 +744,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
* longer required. False otherwise.
|
||||
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
||||
*/
|
||||
protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
|
||||
throws ExoPlaybackException;
|
||||
protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs,
|
||||
MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex,
|
||||
boolean shouldSkip) throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Returns the name of the secure variant of a given decoder.
|
||||
|
@ -225,8 +225,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long startTimeUs, boolean joining) {
|
||||
super.onEnabled(startTimeUs, joining);
|
||||
protected void onEnabled(long positionUs, boolean joining) {
|
||||
super.onEnabled(positionUs, joining);
|
||||
renderedFirstFrame = false;
|
||||
if (joining && allowedJoiningTimeUs > 0) {
|
||||
joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs;
|
||||
@ -234,8 +234,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
super.seekTo(timeUs);
|
||||
protected void seekTo(long positionUs) throws ExoPlaybackException {
|
||||
super.seekTo(positionUs);
|
||||
renderedFirstFrame = false;
|
||||
joiningDeadlineUs = -1;
|
||||
}
|
||||
@ -354,14 +354,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
|
||||
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
|
||||
ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
|
||||
if (shouldSkip) {
|
||||
skipOutputBuffer(codec, bufferIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
long earlyUs = bufferInfo.presentationTimeUs - timeUs;
|
||||
long elapsedSinceStartOfLoop = SystemClock.elapsedRealtime() * 1000 - elapsedRealtimeUs;
|
||||
long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoop;
|
||||
if (earlyUs < -30000) {
|
||||
// We're more than 30ms late rendering the frame.
|
||||
dropOutputBuffer(codec, bufferIndex);
|
||||
|
@ -26,8 +26,12 @@ public class ParserException extends IOException {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ParserException(Exception cause) {
|
||||
public ParserException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -85,9 +85,9 @@ public interface SampleSource {
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @param track The track to enable.
|
||||
* @param timeUs The player's current playback position.
|
||||
* @param positionUs The player's current playback position.
|
||||
*/
|
||||
public void enable(int track, long timeUs);
|
||||
public void enable(int track, long positionUs);
|
||||
|
||||
/**
|
||||
* Disable the specified track.
|
||||
@ -101,12 +101,12 @@ public interface SampleSource {
|
||||
/**
|
||||
* Indicates to the source that it should still be buffering data.
|
||||
*
|
||||
* @param playbackPositionUs The current playback position.
|
||||
* @param positionUs The current playback position.
|
||||
* @return True if the source has available samples, or if the end of the stream has been reached.
|
||||
* False if more data needs to be buffered for samples to become available.
|
||||
* @throws IOException If an error occurred reading from the source.
|
||||
*/
|
||||
public boolean continueBuffering(long playbackPositionUs) throws IOException;
|
||||
public boolean continueBuffering(long positionUs) throws IOException;
|
||||
|
||||
/**
|
||||
* Attempts to read either a sample, a new format or or a discontinuity from the source.
|
||||
@ -118,7 +118,7 @@ public interface SampleSource {
|
||||
* than the one for which data was requested.
|
||||
*
|
||||
* @param track The track from which to read.
|
||||
* @param playbackPositionUs The current playback position.
|
||||
* @param positionUs The current playback position.
|
||||
* @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
|
||||
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
|
||||
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
|
||||
@ -129,7 +129,7 @@ public interface SampleSource {
|
||||
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
|
||||
* @throws IOException If an error occurred reading from the source.
|
||||
*/
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
|
||||
|
||||
/**
|
||||
@ -137,16 +137,16 @@ public interface SampleSource {
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @param timeUs The seek position in microseconds.
|
||||
* @param positionUs The seek position in microseconds.
|
||||
*/
|
||||
public void seekToUs(long timeUs);
|
||||
public void seekToUs(long positionUs);
|
||||
|
||||
/**
|
||||
* Returns an estimate of the position up to which data is buffered.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
||||
* @return An estimate of the absolute position in microseconds up to which data is buffered,
|
||||
* or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or
|
||||
* {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
|
||||
*/
|
||||
|
@ -18,6 +18,8 @@ package com.google.android.exoplayer;
|
||||
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* Renders a single component of media.
|
||||
*
|
||||
@ -59,7 +61,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
*/
|
||||
protected static final int STATE_ENABLED = 2;
|
||||
/**
|
||||
* The renderer is started. Calls to {@link #doSomeWork(long)} should cause the media to be
|
||||
* The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be
|
||||
* rendered.
|
||||
*/
|
||||
protected static final int STATE_STARTED = 3;
|
||||
@ -83,9 +85,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
/**
|
||||
* A time source renderer is a renderer that, when started, advances its own playback position.
|
||||
* This means that {@link #getCurrentPositionUs()} will return increasing positions independently
|
||||
* to increasing values being passed to {@link #doSomeWork(long)}. A player may have at most one
|
||||
* time source renderer. If provided, the player will use such a renderer as its source of time
|
||||
* during playback.
|
||||
* to increasing values being passed to {@link #doSomeWork(long, long)}. A player may have at most
|
||||
* one time source renderer. If provided, the player will use such a renderer as its source of
|
||||
* time during playback.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in any state.
|
||||
*
|
||||
@ -136,15 +138,15 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
/**
|
||||
* Enable the renderer.
|
||||
*
|
||||
* @param timeUs The player's current position.
|
||||
* @param positionUs The player's current position.
|
||||
* @param joining Whether this renderer is being enabled to join an ongoing playback. If true
|
||||
* then {@link #start} must be called immediately after this method returns (unless a
|
||||
* {@link ExoPlaybackException} is thrown).
|
||||
*/
|
||||
/* package */ final void enable(long timeUs, boolean joining) throws ExoPlaybackException {
|
||||
/* package */ final void enable(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||
Assertions.checkState(state == TrackRenderer.STATE_PREPARED);
|
||||
state = TrackRenderer.STATE_ENABLED;
|
||||
onEnabled(timeUs, joining);
|
||||
onEnabled(positionUs, joining);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,18 +154,18 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @param timeUs The player's current position.
|
||||
* @param positionUs The player's current position.
|
||||
* @param joining Whether this renderer is being enabled to join an ongoing playback. If true
|
||||
* then {@link #onStarted} is guaranteed to be called immediately after this method returns
|
||||
* (unless a {@link ExoPlaybackException} is thrown).
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected void onEnabled(long timeUs, boolean joining) throws ExoPlaybackException {
|
||||
protected void onEnabled(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the renderer, meaning that calls to {@link #doSomeWork(long)} will cause the
|
||||
* Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the
|
||||
* track to be rendered.
|
||||
*/
|
||||
/* package */ final void start() throws ExoPlaybackException {
|
||||
@ -289,10 +291,14 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @param timeUs The current playback time.
|
||||
* @param positionUs The current media time in microseconds, measured at the start of the
|
||||
* current iteration of the rendering loop.
|
||||
* @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at
|
||||
* the start of the current iteration of the rendering loop.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected abstract void doSomeWork(long timeUs) throws ExoPlaybackException;
|
||||
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs)
|
||||
throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Returns the duration of the media being rendered.
|
||||
@ -300,7 +306,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST_US} if
|
||||
* @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if
|
||||
* the track's duration should match that of the longest track whose duration is known, or
|
||||
* or {@link #UNKNOWN_TIME_US} if the duration is not known.
|
||||
*/
|
||||
@ -312,17 +318,17 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return The current playback position in micro-seconds.
|
||||
* @return The current playback position in microseconds.
|
||||
*/
|
||||
protected abstract long getCurrentPositionUs();
|
||||
|
||||
/**
|
||||
* Returns an estimate of the absolute position in micro-seconds up to which data is buffered.
|
||||
* Returns an estimate of the absolute position in microseconds up to which data is buffered.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
||||
* @return An estimate of the absolute position in microseconds up to which data is buffered,
|
||||
* or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if
|
||||
* no estimate is available.
|
||||
*/
|
||||
@ -334,10 +340,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}
|
||||
*
|
||||
* @param timeUs The desired time in micro-seconds.
|
||||
* @param positionUs The desired playback position in microseconds.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected abstract void seekTo(long timeUs) throws ExoPlaybackException;
|
||||
protected abstract void seekTo(long positionUs) throws ExoPlaybackException;
|
||||
|
||||
@Override
|
||||
public void handleMessage(int what, Object object) throws ExoPlaybackException {
|
||||
|
@ -154,7 +154,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
private int state;
|
||||
private long downstreamPositionUs;
|
||||
private long lastSeekPositionUs;
|
||||
private long pendingResetTime;
|
||||
private long pendingResetPositionUs;
|
||||
private long lastPerformedBufferOperation;
|
||||
private boolean pendingDiscontinuity;
|
||||
|
||||
@ -219,7 +219,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(int track, long timeUs) {
|
||||
public void enable(int track, long positionUs) {
|
||||
Assertions.checkState(state == STATE_PREPARED);
|
||||
Assertions.checkState(track == 0);
|
||||
state = STATE_ENABLED;
|
||||
@ -227,9 +227,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
loadControl.register(this, bufferSizeContribution);
|
||||
downstreamFormat = null;
|
||||
downstreamMediaFormat = null;
|
||||
downstreamPositionUs = timeUs;
|
||||
lastSeekPositionUs = timeUs;
|
||||
restartFrom(timeUs);
|
||||
downstreamPositionUs = positionUs;
|
||||
lastSeekPositionUs = positionUs;
|
||||
restartFrom(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -253,10 +253,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueBuffering(long playbackPositionUs) throws IOException {
|
||||
public boolean continueBuffering(long positionUs) throws IOException {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
downstreamPositionUs = playbackPositionUs;
|
||||
chunkSource.continueBuffering(playbackPositionUs);
|
||||
downstreamPositionUs = positionUs;
|
||||
chunkSource.continueBuffering(positionUs);
|
||||
updateLoadControl();
|
||||
if (isPendingReset() || mediaChunks.isEmpty()) {
|
||||
return false;
|
||||
@ -271,7 +271,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
Assertions.checkState(track == 0);
|
||||
@ -285,7 +285,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
return NOTHING_READ;
|
||||
}
|
||||
|
||||
downstreamPositionUs = playbackPositionUs;
|
||||
downstreamPositionUs = positionUs;
|
||||
if (isPendingReset()) {
|
||||
if (currentLoadableException != null) {
|
||||
throw currentLoadableException;
|
||||
@ -304,7 +304,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
discardDownstreamMediaChunk();
|
||||
mediaChunk = mediaChunks.getFirst();
|
||||
mediaChunk.seekToStart();
|
||||
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
|
||||
return readData(track, positionUs, formatHolder, sampleHolder, false);
|
||||
} else if (mediaChunk.isLastChunk()) {
|
||||
return END_OF_STREAM;
|
||||
}
|
||||
@ -350,32 +350,32 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToUs(long timeUs) {
|
||||
public void seekToUs(long positionUs) {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
downstreamPositionUs = timeUs;
|
||||
lastSeekPositionUs = timeUs;
|
||||
if (pendingResetTime == timeUs) {
|
||||
downstreamPositionUs = positionUs;
|
||||
lastSeekPositionUs = positionUs;
|
||||
if (pendingResetPositionUs == positionUs) {
|
||||
return;
|
||||
}
|
||||
|
||||
MediaChunk mediaChunk = getMediaChunk(timeUs);
|
||||
MediaChunk mediaChunk = getMediaChunk(positionUs);
|
||||
if (mediaChunk == null) {
|
||||
restartFrom(timeUs);
|
||||
restartFrom(positionUs);
|
||||
pendingDiscontinuity = true;
|
||||
} else {
|
||||
pendingDiscontinuity |= mediaChunk.seekTo(timeUs, mediaChunk == mediaChunks.getFirst());
|
||||
pendingDiscontinuity |= mediaChunk.seekTo(positionUs, mediaChunk == mediaChunks.getFirst());
|
||||
discardDownstreamMediaChunks(mediaChunk);
|
||||
updateLoadControl();
|
||||
}
|
||||
}
|
||||
|
||||
private MediaChunk getMediaChunk(long timeUs) {
|
||||
private MediaChunk getMediaChunk(long positionUs) {
|
||||
Iterator<MediaChunk> mediaChunkIterator = mediaChunks.iterator();
|
||||
while (mediaChunkIterator.hasNext()) {
|
||||
MediaChunk mediaChunk = mediaChunkIterator.next();
|
||||
if (timeUs < mediaChunk.startTimeUs) {
|
||||
if (positionUs < mediaChunk.startTimeUs) {
|
||||
return null;
|
||||
} else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) {
|
||||
} else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) {
|
||||
return mediaChunk;
|
||||
}
|
||||
}
|
||||
@ -386,7 +386,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
public long getBufferedPositionUs() {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
if (isPendingReset()) {
|
||||
return pendingResetTime;
|
||||
return pendingResetPositionUs;
|
||||
}
|
||||
MediaChunk mediaChunk = mediaChunks.getLast();
|
||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||
@ -448,7 +448,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
clearCurrentLoadable();
|
||||
if (state == STATE_ENABLED) {
|
||||
restartFrom(pendingResetTime);
|
||||
restartFrom(pendingResetPositionUs);
|
||||
} else {
|
||||
clearMediaChunks();
|
||||
loadControl.trimAllocator();
|
||||
@ -476,8 +476,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void restartFrom(long timeUs) {
|
||||
pendingResetTime = timeUs;
|
||||
private void restartFrom(long positionUs) {
|
||||
pendingResetPositionUs = positionUs;
|
||||
if (loader.isLoading()) {
|
||||
loader.cancelLoading();
|
||||
} else {
|
||||
@ -499,23 +499,40 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
private void updateLoadControl() {
|
||||
long loadPositionUs;
|
||||
if (isPendingReset()) {
|
||||
loadPositionUs = pendingResetTime;
|
||||
} else {
|
||||
MediaChunk lastMediaChunk = mediaChunks.getLast();
|
||||
loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs;
|
||||
}
|
||||
|
||||
boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal;
|
||||
boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs,
|
||||
isBackedOff || loader.isLoading(), currentLoadableExceptionFatal);
|
||||
|
||||
if (currentLoadableExceptionFatal) {
|
||||
// We've failed, but we still need to update the control with our current state.
|
||||
loadControl.update(this, downstreamPositionUs, -1, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
long nextLoadPositionUs = getNextLoadPositionUs();
|
||||
boolean isBackedOff = currentLoadableException != null;
|
||||
boolean loadingOrBackedOff = loader.isLoading() || isBackedOff;
|
||||
|
||||
// If we're not loading or backed off, evaluate the operation if (a) we don't have the next
|
||||
// chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago.
|
||||
if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1)
|
||||
|| (now - lastPerformedBufferOperation > 2000))) {
|
||||
// Perform the evaluation.
|
||||
lastPerformedBufferOperation = now;
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs,
|
||||
downstreamPositionUs, currentLoadableHolder);
|
||||
boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
|
||||
// Update the next load position as appropriate.
|
||||
if (currentLoadableHolder.chunk == null) {
|
||||
// Set loadPosition to -1 to indicate that we don't have anything to load.
|
||||
nextLoadPositionUs = -1;
|
||||
} else if (chunksDiscarded) {
|
||||
// Chunks were discarded, so we need to re-evaluate the load position.
|
||||
nextLoadPositionUs = getNextLoadPositionUs();
|
||||
}
|
||||
}
|
||||
|
||||
// Update the control with our current state, and determine whether we're the next loader.
|
||||
boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs,
|
||||
loadingOrBackedOff, false);
|
||||
|
||||
if (isBackedOff) {
|
||||
long elapsedMillis = now - currentLoadableExceptionTimestamp;
|
||||
@ -525,17 +542,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loader.isLoading()) {
|
||||
if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) {
|
||||
lastPerformedBufferOperation = now;
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
|
||||
currentLoadableHolder);
|
||||
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
|
||||
}
|
||||
if (nextLoader) {
|
||||
maybeStartLoading();
|
||||
}
|
||||
if (!loader.isLoading() && nextLoader) {
|
||||
maybeStartLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next load time, assuming that the next load starts where the previous chunk ended (or
|
||||
* from the pending reset time, if there is one).
|
||||
*/
|
||||
private long getNextLoadPositionUs() {
|
||||
if (isPendingReset()) {
|
||||
return pendingResetPositionUs;
|
||||
} else {
|
||||
MediaChunk lastMediaChunk = mediaChunks.getLast();
|
||||
return lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs;
|
||||
}
|
||||
}
|
||||
|
||||
@ -552,8 +573,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
Chunk backedOffChunk = currentLoadableHolder.chunk;
|
||||
if (!isMediaChunk(backedOffChunk)) {
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
|
||||
currentLoadableHolder);
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs,
|
||||
downstreamPositionUs, currentLoadableHolder);
|
||||
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
|
||||
if (currentLoadableHolder.chunk == backedOffChunk) {
|
||||
// Chunk was unchanged. Resume loading.
|
||||
@ -577,7 +598,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
MediaChunk removedChunk = mediaChunks.removeLast();
|
||||
Assertions.checkState(backedOffChunk == removedChunk);
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs,
|
||||
currentLoadableHolder);
|
||||
mediaChunks.add(removedChunk);
|
||||
|
||||
@ -603,8 +624,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
if (isMediaChunk(currentLoadable)) {
|
||||
MediaChunk mediaChunk = (MediaChunk) currentLoadable;
|
||||
if (isPendingReset()) {
|
||||
mediaChunk.seekTo(pendingResetTime, false);
|
||||
pendingResetTime = NO_RESET_PENDING;
|
||||
mediaChunk.seekTo(pendingResetPositionUs, false);
|
||||
pendingResetPositionUs = NO_RESET_PENDING;
|
||||
}
|
||||
mediaChunks.add(mediaChunk);
|
||||
notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false,
|
||||
@ -652,10 +673,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
* Discard upstream media chunks until the queue length is equal to the length specified.
|
||||
*
|
||||
* @param queueLength The desired length of the queue.
|
||||
* @return True if chunks were discarded. False otherwise.
|
||||
*/
|
||||
private void discardUpstreamMediaChunks(int queueLength) {
|
||||
private boolean discardUpstreamMediaChunks(int queueLength) {
|
||||
if (mediaChunks.size() <= queueLength) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
long totalBytes = 0;
|
||||
long startTimeUs = 0;
|
||||
@ -667,6 +689,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
removed.release();
|
||||
}
|
||||
notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isMediaChunk(Chunk chunk) {
|
||||
@ -674,7 +697,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
private boolean isPendingReset() {
|
||||
return pendingResetTime != NO_RESET_PENDING;
|
||||
return pendingResetPositionUs != NO_RESET_PENDING;
|
||||
}
|
||||
|
||||
private long getRetryDelayMillis(long errorCount) {
|
||||
@ -757,13 +780,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
||||
}
|
||||
|
||||
private void notifyDownstreamFormatChanged(final String formatId, final int trigger,
|
||||
final long mediaTimeUs) {
|
||||
final long positionUs) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger,
|
||||
usToMs(mediaTimeUs));
|
||||
usToMs(positionUs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -115,43 +115,43 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long timeUs, boolean joining) {
|
||||
source.enable(trackIndex, timeUs);
|
||||
protected void onEnabled(long positionUs, boolean joining) {
|
||||
source.enable(trackIndex, positionUs);
|
||||
parserThread = new HandlerThread("textParser");
|
||||
parserThread.start();
|
||||
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser);
|
||||
seekToInternal(timeUs);
|
||||
seekToInternal(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) {
|
||||
source.seekToUs(timeUs);
|
||||
seekToInternal(timeUs);
|
||||
protected void seekTo(long positionUs) {
|
||||
source.seekToUs(positionUs);
|
||||
seekToInternal(positionUs);
|
||||
}
|
||||
|
||||
private void seekToInternal(long timeUs) {
|
||||
private void seekToInternal(long positionUs) {
|
||||
inputStreamEnded = false;
|
||||
currentPositionUs = timeUs;
|
||||
source.seekToUs(timeUs);
|
||||
if (subtitle != null && (timeUs < subtitle.getStartTime()
|
||||
|| subtitle.getLastEventTime() <= timeUs)) {
|
||||
currentPositionUs = positionUs;
|
||||
source.seekToUs(positionUs);
|
||||
if (subtitle != null && (positionUs < subtitle.getStartTime()
|
||||
|| subtitle.getLastEventTime() <= positionUs)) {
|
||||
subtitle = null;
|
||||
}
|
||||
parserHelper.flush();
|
||||
clearTextRenderer();
|
||||
syncNextEventIndex(timeUs);
|
||||
syncNextEventIndex(positionUs);
|
||||
textRendererNeedsUpdate = subtitle != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
try {
|
||||
source.continueBuffering(timeUs);
|
||||
source.continueBuffering(positionUs);
|
||||
} catch (IOException e) {
|
||||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
|
||||
currentPositionUs = timeUs;
|
||||
currentPositionUs = positionUs;
|
||||
|
||||
if (parserHelper.isParsing()) {
|
||||
return;
|
||||
@ -169,13 +169,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
if (subtitle == null && dequeuedSubtitle != null) {
|
||||
// We've dequeued a new subtitle. Sync the event index and update the subtitle.
|
||||
subtitle = dequeuedSubtitle;
|
||||
syncNextEventIndex(timeUs);
|
||||
syncNextEventIndex(positionUs);
|
||||
textRendererNeedsUpdate = true;
|
||||
} else if (subtitle != null) {
|
||||
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
|
||||
// advance to the next event.
|
||||
long nextEventTimeUs = getNextEventTime();
|
||||
while (nextEventTimeUs <= timeUs) {
|
||||
while (nextEventTimeUs <= positionUs) {
|
||||
nextSubtitleEventIndex++;
|
||||
nextEventTimeUs = getNextEventTime();
|
||||
textRendererNeedsUpdate = true;
|
||||
@ -191,7 +191,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
if (subtitle == null) {
|
||||
try {
|
||||
SampleHolder sampleHolder = parserHelper.getSampleHolder();
|
||||
int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false);
|
||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
parserHelper.startParseOperation();
|
||||
} else if (result == SampleSource.END_OF_STREAM) {
|
||||
@ -208,7 +208,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
if (subtitle == null) {
|
||||
clearTextRenderer();
|
||||
} else {
|
||||
updateTextRenderer(timeUs);
|
||||
updateTextRenderer(positionUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,8 +256,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void syncNextEventIndex(long timeUs) {
|
||||
nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(timeUs);
|
||||
private void syncNextEventIndex(long positionUs) {
|
||||
nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(positionUs);
|
||||
}
|
||||
|
||||
private long getNextEventTime() {
|
||||
@ -266,8 +266,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
: (subtitle.getEventTime(nextSubtitleEventIndex));
|
||||
}
|
||||
|
||||
private void updateTextRenderer(long timeUs) {
|
||||
String text = subtitle.getText(timeUs);
|
||||
private void updateTextRenderer(long positionUs) {
|
||||
String text = subtitle.getText(positionUs);
|
||||
log("updateTextRenderer; text=: " + text);
|
||||
if (textRendererHandler != null) {
|
||||
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget();
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.text.ttml;
|
||||
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.text.Subtitle;
|
||||
import com.google.android.exoplayer.text.SubtitleParser;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
@ -72,8 +73,23 @@ public class TtmlParser implements SubtitleParser {
|
||||
private static final int DEFAULT_TICKRATE = 1;
|
||||
|
||||
private final XmlPullParserFactory xmlParserFactory;
|
||||
private final boolean strictParsing;
|
||||
|
||||
/**
|
||||
* Equivalent to {@code TtmlParser(true)}.
|
||||
*/
|
||||
public TtmlParser() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param strictParsing If true, {@link #parse(InputStream, String, long)} will throw a
|
||||
* {@link ParserException} if the stream contains invalid ttml. If false, the parser will
|
||||
* make a best effort to ignore minor errors in the stream. Note however that a
|
||||
* {@link ParserException} will still be thrown when this is not possible.
|
||||
*/
|
||||
public TtmlParser(boolean strictParsing) {
|
||||
this.strictParsing = strictParsing;
|
||||
try {
|
||||
xmlParserFactory = XmlPullParserFactory.newInstance();
|
||||
} catch (XmlPullParserException e) {
|
||||
@ -89,21 +105,31 @@ public class TtmlParser implements SubtitleParser {
|
||||
xmlParser.setInput(inputStream, inputEncoding);
|
||||
TtmlSubtitle ttmlSubtitle = null;
|
||||
LinkedList<TtmlNode> nodeStack = new LinkedList<TtmlNode>();
|
||||
int unsupportedTagDepth = 0;
|
||||
int unsupportedNodeDepth = 0;
|
||||
int eventType = xmlParser.getEventType();
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
TtmlNode parent = nodeStack.peekLast();
|
||||
if (unsupportedTagDepth == 0) {
|
||||
if (unsupportedNodeDepth == 0) {
|
||||
String name = xmlParser.getName();
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
if (!isSupportedTag(name)) {
|
||||
Log.w(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
|
||||
unsupportedTagDepth++;
|
||||
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
|
||||
unsupportedNodeDepth++;
|
||||
} else {
|
||||
TtmlNode node = parseNode(xmlParser, parent);
|
||||
nodeStack.addLast(node);
|
||||
if (parent != null) {
|
||||
parent.addChild(node);
|
||||
try {
|
||||
TtmlNode node = parseNode(xmlParser, parent);
|
||||
nodeStack.addLast(node);
|
||||
if (parent != null) {
|
||||
parent.addChild(node);
|
||||
}
|
||||
} catch (ParserException e) {
|
||||
if (strictParsing) {
|
||||
throw e;
|
||||
} else {
|
||||
Log.e(TAG, "Suppressing parser error", e);
|
||||
// Treat the node (and by extension, all of its children) as unsupported.
|
||||
unsupportedNodeDepth++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (eventType == XmlPullParser.TEXT) {
|
||||
@ -116,9 +142,9 @@ public class TtmlParser implements SubtitleParser {
|
||||
}
|
||||
} else {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
unsupportedTagDepth++;
|
||||
unsupportedNodeDepth++;
|
||||
} else if (eventType == XmlPullParser.END_TAG) {
|
||||
unsupportedTagDepth--;
|
||||
unsupportedNodeDepth--;
|
||||
}
|
||||
}
|
||||
xmlParser.next();
|
||||
@ -126,7 +152,7 @@ public class TtmlParser implements SubtitleParser {
|
||||
}
|
||||
return ttmlSubtitle;
|
||||
} catch (XmlPullParserException xppe) {
|
||||
throw new IOException("Unable to parse source", xppe);
|
||||
throw new ParserException("Unable to parse source", xppe);
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +161,7 @@ public class TtmlParser implements SubtitleParser {
|
||||
return MimeTypes.APPLICATION_TTML.equals(mimeType);
|
||||
}
|
||||
|
||||
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) {
|
||||
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException {
|
||||
long duration = 0;
|
||||
long startTime = TtmlNode.UNDEFINED_TIME;
|
||||
long endTime = TtmlNode.UNDEFINED_TIME;
|
||||
@ -209,10 +235,10 @@ public class TtmlParser implements SubtitleParser {
|
||||
* @param subframeRate The sub-framerate of the stream
|
||||
* @param tickRate The tick rate of the stream.
|
||||
* @return The parsed timestamp in microseconds.
|
||||
* @throws NumberFormatException If the given string does not contain a valid time expression.
|
||||
* @throws ParserException If the given string does not contain a valid time expression.
|
||||
*/
|
||||
private static long parseTimeExpression(String time, int frameRate, int subframeRate,
|
||||
int tickRate) {
|
||||
int tickRate) throws ParserException {
|
||||
Matcher matcher = CLOCK_TIME.matcher(time);
|
||||
if (matcher.matches()) {
|
||||
String hours = matcher.group(1);
|
||||
@ -250,7 +276,7 @@ public class TtmlParser implements SubtitleParser {
|
||||
}
|
||||
return (long) (offsetSeconds * 1000000);
|
||||
}
|
||||
throw new NumberFormatException("Malformed time expression: " + time);
|
||||
throw new ParserException("Malformed time expression: " + time);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user