diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..16f7e8aec9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Android generated +bin +gen +lint.xml + +# IntelliJ IDEA +.idea +*.iml +*.ipr +*.iws +classes +gen-external-apklibs + +# Eclipse +.project +.classpath +.settings +.checkstyle + +# Gradle +.gradle +build +out + +# Maven +target +release.properties +pom.xml.* + +# Ant +ant.properties +local.properties +proguard.cfg +proguard-project.txt + +# Other +.DS_Store +dist +tmp diff --git a/README.md b/README.md index 7efc281f85..6faf3a264b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,22 @@ accompanying demo application. To get started: ## Using Gradle ## -ExoPlayer can also be built using Gradle. For a complete list of tasks, run: +ExoPlayer can also be built using Gradle. You can include it as a dependent project and build from source. e.g. -./gradlew tasks +``` +// setting.gradle +include ':app', ':..:ExoPlayer:library' + +// app/build.gradle +dependencies { + compile project(':..:ExoPlayer:library') +} +``` + +If you want to use ExoPlayer as a jar, run: + +``` +./gradlew jarRelease +``` + +and copy library.jar to the libs-folder of your new project. diff --git a/build.gradle b/build.gradle index 2d29f854be..a444cfb512 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.10.+' + classpath 'com.android.tools.build:gradle:0.12.+' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 330adc6d2b..98ed2f18a9 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java index f8306d10d1..722e21c45e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java @@ -80,9 +80,9 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener // DemoPlayer.InfoListener @Override - public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) { + public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + - ", " + getTimeString(elapsedMs) + ", " + bandwidthEstimate + "]"); + ", " + getTimeString(elapsedMs) + ", " + bitrateEstimate + "]"); } @Override @@ -92,7 +92,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener @Override public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, - int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { + int mediaStartTimeMs, int mediaEndTimeMs, long length) { loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); if (VerboseLogUtil.isTagEnabled(TAG)) { Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId @@ -101,7 +101,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } @Override - public void onLoadCompleted(int sourceId) { + public void onLoadCompleted(int sourceId, long bytesLoaded) { if (VerboseLogUtil.isTagEnabled(TAG)) { long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java index 498e087d12..8093bad814 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java @@ -98,12 +98,12 @@ import android.widget.TextView; @Override protected long getDurationUs() { - return TrackRenderer.MATCH_LONGEST; + return TrackRenderer.MATCH_LONGEST_US; } @Override protected long getBufferedPositionUs() { - return TrackRenderer.END_OF_TRACK; + return TrackRenderer.END_OF_TRACK_US; } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index acf69656ed..bdaa2e5a73 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -121,10 +121,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onDroppedFrames(int count, long elapsed); - void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate); + void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, - int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); - void onLoadCompleted(int sourceId); + int mediaStartTimeMs, int mediaEndTimeMs, long length); + void onLoadCompleted(int sourceId, long bytesLoaded); } /** @@ -391,9 +391,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) { + public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { if (infoListener != null) { - infoListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate); + infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate); } } @@ -471,34 +471,34 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, - int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { + int mediaStartTimeMs, int mediaEndTimeMs, long length) { if (infoListener != null) { infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, - mediaEndTimeMs, totalBytes); + mediaEndTimeMs, length); } } @Override - public void onLoadCompleted(int sourceId) { + public void onLoadCompleted(int sourceId, long bytesLoaded) { if (infoListener != null) { - infoListener.onLoadCompleted(sourceId); + infoListener.onLoadCompleted(sourceId, bytesLoaded); } } @Override - public void onLoadCanceled(int sourceId) { + public void onLoadCanceled(int sourceId, long bytesLoaded) { // Do nothing. } @Override public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, - long totalBytes) { + long bytesDiscarded) { // Do nothing. } @Override public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, - long totalBytes) { + long bytesDiscarded) { // Do nothing. } diff --git a/library/build.gradle b/library/build.gradle index 5b751a0820..68a489b08a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -36,3 +36,14 @@ android { dependencies { } + +android.libraryVariants.all { variant -> + def name = variant.buildType.name + if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) { + return; // Skip debug builds. + } + def task = project.tasks.create "jar${name.capitalize()}", Jar + task.dependsOn variant.javaCompile + task.from variant.javaCompile.destinationDir + artifacts.add('archives', task); +} diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java new file mode 100644 index 0000000000..c8cd9fe586 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 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.exoplayer; + +/** + * Defines constants that are generally useful throughout the library. + */ +public final class C { + + /** + * Represents an unbounded length of data. + */ + public static final int LENGTH_UNBOUNDED = -1; + + private C() {} + +} diff --git a/library/src/main/java/com/google/android/exoplayer/CodecCounters.java b/library/src/main/java/com/google/android/exoplayer/CodecCounters.java index 7136ef2b1c..6b12b9c072 100644 --- a/library/src/main/java/com/google/android/exoplayer/CodecCounters.java +++ b/library/src/main/java/com/google/android/exoplayer/CodecCounters.java @@ -17,54 +17,41 @@ package com.google.android.exoplayer; /** * Maintains codec event counts, for debugging purposes only. + *

+ * Counters should be written from the playback thread only. Counters may be read from any thread. + * To ensure that the counter values are correctly reflected between threads, users of this class + * should invoke {@link #ensureUpdated()} prior to reading and after writing. */ public final class CodecCounters { - public volatile long codecInitCount; - public volatile long codecReleaseCount; - public volatile long outputFormatChangedCount; - public volatile long outputBuffersChangedCount; - public volatile long queuedInputBufferCount; - public volatile long inputBufferWaitingForSampleCount; - public volatile long keyframeCount; - public volatile long queuedEndOfStreamCount; - public volatile long renderedOutputBufferCount; - public volatile long skippedOutputBufferCount; - public volatile long droppedOutputBufferCount; - public volatile long discardedSamplesCount; + public int codecInitCount; + public int codecReleaseCount; + public int outputFormatChangedCount; + public int outputBuffersChangedCount; + public int renderedOutputBufferCount; + public int skippedOutputBufferCount; + public int droppedOutputBufferCount; /** - * Resets all counts to zero. + * Should be invoked from the playback thread after the counters have been updated. Should also + * be invoked from any other thread that wishes to read the counters, before reading. These calls + * ensure that counter updates are made visible to the reading threads. */ - public void zeroAllCounts() { - codecInitCount = 0; - codecReleaseCount = 0; - outputFormatChangedCount = 0; - outputBuffersChangedCount = 0; - queuedInputBufferCount = 0; - inputBufferWaitingForSampleCount = 0; - keyframeCount = 0; - queuedEndOfStreamCount = 0; - renderedOutputBufferCount = 0; - skippedOutputBufferCount = 0; - droppedOutputBufferCount = 0; - discardedSamplesCount = 0; + public synchronized void ensureUpdated() { + // Do nothing. The use of synchronized ensures a memory barrier should another thread also + // call this method. } public String getDebugString() { + ensureUpdated(); StringBuilder builder = new StringBuilder(); builder.append("cic(").append(codecInitCount).append(")"); builder.append("crc(").append(codecReleaseCount).append(")"); builder.append("ofc(").append(outputFormatChangedCount).append(")"); builder.append("obc(").append(outputBuffersChangedCount).append(")"); - builder.append("qib(").append(queuedInputBufferCount).append(")"); - builder.append("wib(").append(inputBufferWaitingForSampleCount).append(")"); - builder.append("kfc(").append(keyframeCount).append(")"); - builder.append("qes(").append(queuedEndOfStreamCount).append(")"); builder.append("ren(").append(renderedOutputBufferCount).append(")"); builder.append("sob(").append(skippedOutputBufferCount).append(")"); builder.append("dob(").append(droppedOutputBufferCount).append(")"); - builder.append("dsc(").append(discardedSamplesCount).append(")"); return builder.toString(); } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java index 15288280aa..244a31eaf5 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java @@ -316,14 +316,16 @@ public interface ExoPlayer { public void seekTo(int positionMs); /** - * Stops playback. + * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention + * is to pause playback. *

* Calling this method will cause the playback state to transition to - * {@link ExoPlayer#STATE_IDLE}. Note that the player instance can still be used, and that - * {@link ExoPlayer#release()} must still be called on the player should it no longer be required. + * {@link ExoPlayer#STATE_IDLE}. The player instance can still be used, and + * {@link ExoPlayer#release()} must still be called on the player if it's no longer required. *

- * Use {@code setPlayWhenReady(false)} rather than this method if the intention is to pause - * playback. + * Calling this method does not reset the playback position. If this player instance will be used + * to play another video from its start, then {@code seekTo(0)} should be called after stopping + * the player and before preparing it for the next video. */ public void stop(); diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index c6858eef3c..9dc6228fa9 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -60,7 +60,7 @@ import java.util.List; private static final int IDLE_INTERVAL_MS = 1000; private final Handler handler; - private final HandlerThread internalPlayerThread; + private final HandlerThread internalPlaybackThread; private final Handler eventHandler; private final MediaClock mediaClock; private final boolean[] rendererEnabledFlags; @@ -95,12 +95,12 @@ import java.util.List; } this.state = ExoPlayer.STATE_IDLE; - this.durationUs = TrackRenderer.UNKNOWN_TIME; - this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME; + this.durationUs = TrackRenderer.UNKNOWN_TIME_US; + this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US; mediaClock = new MediaClock(); enabledRenderers = new ArrayList(rendererEnabledFlags.length); - internalPlayerThread = new HandlerThread(getClass().getSimpleName() + ":Handler") { + internalPlaybackThread = new HandlerThread(getClass().getSimpleName() + ":Handler") { @Override public void run() { // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can @@ -109,12 +109,12 @@ import java.util.List; super.run(); } }; - internalPlayerThread.start(); - handler = new Handler(internalPlayerThread.getLooper(), this); + internalPlaybackThread.start(); + handler = new Handler(internalPlaybackThread.getLooper(), this); } public Looper getPlaybackLooper() { - return internalPlayerThread.getLooper(); + return internalPlaybackThread.getLooper(); } public int getCurrentPosition() { @@ -122,12 +122,12 @@ import java.util.List; } public int getBufferedPosition() { - return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME + return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME : (int) (bufferedPositionUs / 1000); } public int getDuration() { - return durationUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME + return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME : (int) (durationUs / 1000); } @@ -179,7 +179,7 @@ import java.util.List; Thread.currentThread().interrupt(); } } - internalPlayerThread.quit(); + internalPlaybackThread.quit(); } } @@ -287,14 +287,14 @@ import java.util.List; enabledRenderers.add(renderer); isEnded = isEnded && renderer.isEnded(); allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); - if (durationUs == TrackRenderer.UNKNOWN_TIME) { + if (durationUs == TrackRenderer.UNKNOWN_TIME_US) { // We've already encountered a track for which the duration is unknown, so the media // duration is unknown regardless of the duration of this track. } else { long trackDurationUs = renderer.getDurationUs(); - if (trackDurationUs == TrackRenderer.UNKNOWN_TIME) { - durationUs = TrackRenderer.UNKNOWN_TIME; - } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST) { + if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) { + durationUs = TrackRenderer.UNKNOWN_TIME_US; + } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) { // Do nothing. } else { durationUs = Math.max(durationUs, trackDurationUs); @@ -331,11 +331,11 @@ import java.util.List; long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs; return minBufferDurationUs <= 0 - || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME - || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK + || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US + || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US || rendererBufferedPositionUs >= positionUs + minBufferDurationUs - || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME - && rendererDurationUs != TrackRenderer.MATCH_LONGEST + || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US + && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US && rendererBufferedPositionUs >= rendererDurationUs); } @@ -384,7 +384,7 @@ import java.util.List; private void doSomeWork() throws ExoPlaybackException { TraceUtil.beginSection("doSomeWork"); long operationStartTimeMs = SystemClock.elapsedRealtime(); - long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME ? durationUs + long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs : Long.MAX_VALUE; boolean isEnded = true; boolean allRenderersReadyOrEnded = true; @@ -398,17 +398,17 @@ import java.util.List; isEnded = isEnded && renderer.isEnded(); allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); - if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME) { + if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) { // We've already encountered a track for which the buffered position is unknown. Hence the // media buffer position unknown regardless of the buffered position of this track. } else { long rendererDurationUs = renderer.getDurationUs(); long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); - if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME) { - bufferedPositionUs = TrackRenderer.UNKNOWN_TIME; - } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK - || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME - && rendererDurationUs != TrackRenderer.MATCH_LONGEST + if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) { + bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US; + } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US + || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US + && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US && rendererBufferedPositionUs >= rendererDurationUs)) { // This track is fully buffered. } else { @@ -525,7 +525,7 @@ import java.util.List; notifyAll(); } } - if (state != ExoPlayer.STATE_IDLE) { + if (state != ExoPlayer.STATE_IDLE && state != ExoPlayer.STATE_PREPARING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 4b50a56517..06973db73b 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.0.11"; + public static final String VERSION = "1.0.12"; /** * The version of the library, expressed as an integer. @@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 1002003. */ - public static final int VERSION_INT = 1000010; + public static final int VERSION_INT = 1000012; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 653dc2d628..908664495f 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -67,12 +67,12 @@ public final class FrameworkSampleSource implements SampleSource { extractor = new MediaExtractor(); extractor.setDataSource(context, uri, headers); trackStates = new int[extractor.getTrackCount()]; - pendingDiscontinuities = new boolean[extractor.getTrackCount()]; + pendingDiscontinuities = new boolean[trackStates.length]; trackInfos = new TrackInfo[trackStates.length]; for (int i = 0; i < trackStates.length; i++) { android.media.MediaFormat format = extractor.getTrackFormat(i); long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? - format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME; + format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME_US; String mime = format.getString(android.media.MediaFormat.KEY_MIME); trackInfos[i] = new TrackInfo(mime, duration); } @@ -84,7 +84,7 @@ public final class FrameworkSampleSource implements SampleSource { @Override public int getTrackCount() { Assertions.checkState(prepared); - return extractor.getTrackCount(); + return trackStates.length; } @Override @@ -97,17 +97,18 @@ public final class FrameworkSampleSource implements SampleSource { public void enable(int track, long timeUs) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); - boolean wasSourceEnabled = isEnabled(); trackStates[track] = TRACK_STATE_ENABLED; extractor.selectTrack(track); - if (!wasSourceEnabled) { - seekToUs(timeUs); - } + seekToUs(timeUs); } @Override - public void continueBuffering(long playbackPositionUs) { - // Do nothing. The MediaExtractor instance is responsible for buffering. + public boolean continueBuffering(long playbackPositionUs) { + // 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 + // the TODO. + return true; } @Override @@ -122,15 +123,15 @@ public final class FrameworkSampleSource implements SampleSource { if (onlyReadDiscontinuity) { return NOTHING_READ; } + if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { + formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16( + extractor.getTrackFormat(track)); + formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null; + trackStates[track] = TRACK_STATE_FORMAT_SENT; + return FORMAT_READ; + } int extractorTrackIndex = extractor.getSampleTrackIndex(); if (extractorTrackIndex == track) { - if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { - formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16( - extractor.getTrackFormat(track)); - formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null; - trackStates[track] = TRACK_STATE_FORMAT_SENT; - return FORMAT_READ; - } if (sampleHolder.data != null) { int offset = sampleHolder.data.position(); sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset); @@ -187,7 +188,7 @@ public final class FrameworkSampleSource implements SampleSource { Assertions.checkState(prepared); long bufferedDurationUs = extractor.getCachedDuration(); if (bufferedDurationUs == -1) { - return TrackRenderer.UNKNOWN_TIME; + return TrackRenderer.UNKNOWN_TIME_US; } else { return extractor.getSampleTime() + bufferedDurationUs; } @@ -202,13 +203,4 @@ public final class FrameworkSampleSource implements SampleSource { } } - private boolean isEnabled() { - for (int i = 0; i < trackStates.length; i++) { - if (trackStates[i] != TRACK_STATE_DISABLED) { - return true; - } - } - return false; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index 2ffe4a53f6..798282dd91 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -266,8 +266,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onOutputFormatChanged(MediaFormat format) { - releaseAudioTrack(); - this.sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelConfig; switch (channelCount) { @@ -283,6 +281,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { default: throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } + + int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + if (audioTrack != null && this.sampleRate == sampleRate + && this.channelConfig == channelConfig) { + // We already have an existing audio track with the correct sample rate and channel config. + return; + } + + releaseAudioTrack(); + this.sampleRate = sampleRate; this.channelConfig = channelConfig; this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT); @@ -417,7 +425,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean isReady() { - return getPendingFrameCount() > 0; + return super.isReady() || getPendingFrameCount() > 0; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 99a8bee91e..6f7262f79a 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -128,6 +128,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { private int codecReconfigurationState; private int trackIndex; + private boolean sourceIsReady; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -186,7 +187,12 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { return TrackRenderer.STATE_IGNORE; } - @SuppressWarnings("unused") + /** + * Determines whether a mime type is handled by the renderer. + * + * @param mimeType The mime type to test. + * @return True if the renderer can handle the mime type. False otherwise. + */ protected boolean handlesMimeType(String mimeType) { return true; // TODO: Uncomment once the TODO above is fixed. @@ -196,6 +202,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected void onEnabled(long timeUs, boolean joining) { source.enable(trackIndex, timeUs); + sourceIsReady = false; inputStreamEnded = false; outputStreamEnded = false; waitingForKeys = false; @@ -280,14 +287,20 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected void onDisabled() { - releaseCodec(); format = null; drmInitData = null; - if (openedDrmSession) { - drmSessionManager.close(); - openedDrmSession = false; + try { + releaseCodec(); + } finally { + try { + if (openedDrmSession) { + drmSessionManager.close(); + openedDrmSession = false; + } + } finally { + source.disable(trackIndex); + } } - source.disable(trackIndex); } protected void releaseCodec() { @@ -332,7 +345,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected long getBufferedPositionUs() { long sourceBufferedPosition = source.getBufferedPositionUs(); - return sourceBufferedPosition == UNKNOWN_TIME || sourceBufferedPosition == END_OF_TRACK + return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US ? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs()); } @@ -340,6 +353,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { protected void seekTo(long timeUs) throws ExoPlaybackException { currentPositionUs = timeUs; source.seekToUs(timeUs); + sourceIsReady = false; inputStreamEnded = false; outputStreamEnded = false; waitingForKeys = false; @@ -358,7 +372,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected void doSomeWork(long timeUs) throws ExoPlaybackException { try { - source.continueBuffering(timeUs); + sourceIsReady = source.continueBuffering(timeUs); checkForDiscontinuity(); if (format == null) { readFormat(); @@ -373,6 +387,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { while (feedInputBuffer()) {} } } + codecCounters.ensureUpdated(); } catch (IOException e) { throw new ExoPlaybackException(e); } @@ -394,7 +409,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { if (!sampleHolder.decodeOnly) { currentPositionUs = sampleHolder.timeUs; } - codecCounters.discardedSamplesCount++; } else if (result == SampleSource.FORMAT_READ) { onInputFormatChanged(formatHolder); } @@ -467,7 +481,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } if (result == SampleSource.NOTHING_READ) { - codecCounters.inputBufferWaitingForSampleCount++; return false; } if (result == SampleSource.DISCONTINUITY_READ) { @@ -496,7 +509,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { try { codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); inputIndex = -1; - codecCounters.queuedEndOfStreamCount++; } catch (CryptoException e) { notifyCryptoError(e); throw new ExoPlaybackException(e); @@ -536,10 +548,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } else { codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0); } - codecCounters.queuedInputBufferCount++; - if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { - codecCounters.keyframeCount++; - } inputIndex = -1; codecReconfigurationState = RECONFIGURATION_STATE_NONE; } catch (CryptoException e) { @@ -625,7 +633,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @param newFormat The new format. * @return True if the existing instance can be reconfigured. False otherwise. */ - @SuppressWarnings("unused") protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, MediaFormat oldFormat, MediaFormat newFormat) { return false; @@ -639,10 +646,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected boolean isReady() { return format != null && !waitingForKeys - && ((codec == null && !shouldInitCodec()) // We don't want the codec - || outputIndex >= 0 // Or we have an output buffer ready to release - || inputIndex < 0 // Or we don't have any input buffers to write to - || isWithinHotswapPeriod()); // Or the codec is being hotswapped + && (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod()); } private boolean isWithinHotswapPeriod() { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 98ec611d2a..b941767955 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -235,7 +235,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean isReady() { - if (super.isReady() && (renderedFirstFrame || !codecInitialized())) { + if (super.isReady()) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineUs = -1; return true; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index f53defdf93..3188e36db0 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -148,12 +148,25 @@ public class MediaFormat { if (obj == null || getClass() != obj.getClass()) { return false; } - MediaFormat other = (MediaFormat) obj; - if (maxInputSize != other.maxInputSize || width != other.width || height != other.height || - maxWidth != other.maxWidth || maxHeight != other.maxHeight || - channelCount != other.channelCount || sampleRate != other.sampleRate || - !Util.areEqual(mimeType, other.mimeType) || - initializationData.size() != other.initializationData.size()) { + return equalsInternal((MediaFormat) obj, false); + } + + public boolean equals(MediaFormat other, boolean ignoreMaxDimensions) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + return equalsInternal(other, ignoreMaxDimensions); + } + + private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) { + if (maxInputSize != other.maxInputSize || width != other.width || height != other.height + || (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight)) + || channelCount != other.channelCount || sampleRate != other.sampleRate + || !Util.areEqual(mimeType, other.mimeType) + || initializationData.size() != other.initializationData.size()) { return false; } for (int i = 0; i < initializationData.size(); i++) { diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 9c5d6aa303..fc29ef1ad5 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -102,8 +102,11 @@ public interface SampleSource { * Indicates to the source that it should still be buffering data. * * @param playbackPositionUs 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 void continueBuffering(long playbackPositionUs); + public boolean continueBuffering(long playbackPositionUs) throws IOException; /** * Attempts to read either a sample, a new format or or a discontinuity from the source. @@ -144,8 +147,8 @@ public interface SampleSource { * 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, - * or {@link TrackRenderer#END_OF_TRACK} if data is buffered to the end of the stream, or - * {@link TrackRenderer#UNKNOWN_TIME} if no estimate is available. + * 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. */ public long getBufferedPositionUs(); diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index a8c08cf4f0..f27433d06e 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -67,16 +67,16 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * Represents an unknown time or duration. */ - public static final long UNKNOWN_TIME = -1; + public static final long UNKNOWN_TIME_US = -1; /** * Represents a time or duration that should match the duration of the longest track whose * duration is known. */ - public static final long MATCH_LONGEST = -2; + public static final long MATCH_LONGEST_US = -2; /** * Represents the time of the end of the track. */ - public static final long END_OF_TRACK = -3; + public static final long END_OF_TRACK_US = -3; private int state; @@ -110,7 +110,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * * @return The current state (one of the STATE_* constants), for convenience. */ - @SuppressWarnings("unused") /* package */ final int prepare() throws ExoPlaybackException { Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED); state = doPrepare(); @@ -301,9 +300,9 @@ 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} if + * @return The duration of the track in micro-seconds, 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} if the duration is not known. + * or {@link #UNKNOWN_TIME_US} if the duration is not known. */ protected abstract long getDurationUs(); @@ -324,8 +323,8 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * * @return An estimate of the absolute position in micro-seconds up to which data is buffered, - * or {@link #END_OF_TRACK} if the track is fully buffered, or {@link #UNKNOWN_TIME} if no - * estimate is available. + * or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if + * no estimate is available. */ protected abstract long getBufferedPositionUs(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java index 72a95232c5..6724e283fb 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.chunk; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.Allocation; import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; @@ -51,7 +52,7 @@ public abstract class Chunk implements Loadable { /** * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed - * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * the length resolved by {@code dataSource.open(dataSpec)} must not exceed * {@link Integer#MAX_VALUE}. * @param format See {@link #format}. @@ -89,8 +90,8 @@ public abstract class Chunk implements Loadable { /** * Gets the length of the chunk in bytes. * - * @return The length of the chunk in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length - * has yet to be determined. + * @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet + * to be determined. */ public final long getLength() { return dataSourceStream.getLength(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 61ad0c93ee..980de4ec23 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.chunk; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaFormat; @@ -22,7 +23,6 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.util.Assertions; @@ -57,24 +57,27 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { * load is for initialization data. * @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this * load is for initialization data. - * @param totalBytes The length of the data being loaded in bytes. + * @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if + * the length of the data has not yet been determined. */ void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, - int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); + int mediaStartTimeMs, int mediaEndTimeMs, long length); /** * Invoked when the current load operation completes. * * @param sourceId The id of the reporting {@link SampleSource}. + * @param bytesLoaded The number of bytes that were loaded. */ - void onLoadCompleted(int sourceId); + void onLoadCompleted(int sourceId, long bytesLoaded); /** * Invoked when the current upstream load operation is canceled. * * @param sourceId The id of the reporting {@link SampleSource}. + * @param bytesLoaded The number of bytes that were loaded prior to the cancellation. */ - void onLoadCanceled(int sourceId); + void onLoadCanceled(int sourceId, long bytesLoaded); /** * Invoked when data is removed from the back of the buffer, typically so that it can be @@ -83,10 +86,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { * @param sourceId The id of the reporting {@link SampleSource}. * @param mediaStartTimeMs The media time of the start of the discarded data. * @param mediaEndTimeMs The media time of the end of the discarded data. - * @param totalBytes The length of the data being discarded in bytes. + * @param bytesDiscarded The length of the data being discarded in bytes. */ void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, - long totalBytes); + long bytesDiscarded); /** * Invoked when an error occurs loading media data. @@ -111,10 +114,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { * @param sourceId The id of the reporting {@link SampleSource}. * @param mediaStartTimeMs The media time of the start of the discarded data. * @param mediaEndTimeMs The media time of the end of the discarded data. - * @param totalBytes The length of the data being discarded in bytes. + * @param bytesDiscarded The length of the data being discarded in bytes. */ void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, - long totalBytes); + long bytesDiscarded); /** * Invoked when the downstream format changes (i.e. when the format being supplied to the @@ -246,11 +249,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } @Override - public void continueBuffering(long playbackPositionUs) { + public boolean continueBuffering(long playbackPositionUs) throws IOException { Assertions.checkState(state == STATE_ENABLED); downstreamPositionUs = playbackPositionUs; chunkSource.continueBuffering(playbackPositionUs); updateLoadControl(); + if (isPendingReset() || mediaChunks.isEmpty()) { + return false; + } else if (mediaChunks.getFirst().sampleAvailable()) { + // There's a sample available to be read from the current chunk. + return true; + } else { + // It may be the case that the current chunk has been fully read but not yet discarded and + // that the next chunk has an available sample. Return true if so, otherwise false. + return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); + } } @Override @@ -309,7 +322,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } MediaFormat mediaFormat = mediaChunk.getMediaFormat(); - if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) { + if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) { chunkSource.getMaxVideoDimensions(mediaFormat); formatHolder.format = mediaFormat; formatHolder.drmInitData = mediaChunk.getPsshInfo(); @@ -373,14 +386,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { if (currentLoadable != null && mediaChunk == currentLoadable) { // Linearly interpolate partially-fetched chunk times. long chunkLength = mediaChunk.getLength(); - if (chunkLength != DataSpec.LENGTH_UNBOUNDED) { + if (chunkLength != C.LENGTH_UNBOUNDED) { return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) * mediaChunk.bytesLoaded()) / chunkLength; } else { return mediaChunk.startTimeUs; } } else if (mediaChunk.isLastChunk()) { - return TrackRenderer.END_OF_TRACK; + return TrackRenderer.END_OF_TRACK_US; } else { return mediaChunk.endTimeUs; } @@ -399,6 +412,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { @Override public void onLoaded() { Chunk currentLoadable = currentLoadableHolder.chunk; + notifyLoadCompleted(currentLoadable.bytesLoaded()); try { currentLoadable.consume(); } catch (IOException e) { @@ -414,7 +428,6 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } - notifyLoadCompleted(); updateLoadControl(); } } @@ -422,11 +435,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { @Override public void onCanceled() { Chunk currentLoadable = currentLoadableHolder.chunk; + notifyLoadCanceled(currentLoadable.bytesLoaded()); if (!isMediaChunk(currentLoadable)) { currentLoadable.release(); } clearCurrentLoadable(); - notifyLoadCanceled(); if (state == STATE_ENABLED) { restartFrom(pendingResetTime); } else { @@ -667,35 +680,35 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { private void notifyLoadStarted(final String formatId, final int trigger, final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs, - final long totalBytes) { + final long length) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization, - usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), totalBytes); + usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), length); } }); } } - private void notifyLoadCompleted() { + private void notifyLoadCompleted(final long bytesLoaded) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onLoadCompleted(eventSourceId); + eventListener.onLoadCompleted(eventSourceId, bytesLoaded); } }); } } - private void notifyLoadCanceled() { + private void notifyLoadCanceled(final long bytesLoaded) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onLoadCanceled(eventSourceId); + eventListener.onLoadCanceled(eventSourceId, bytesLoaded); } }); } @@ -750,13 +763,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs, - final long totalBytes) { + final long bytesDiscarded) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), - usToMs(mediaEndTimeUs), totalBytes); + usToMs(mediaEndTimeUs), bytesDiscarded); } }); } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java index 875956c0ee..3482d160fc 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java @@ -71,6 +71,14 @@ public class Format { */ public final int bitrate; + /** + * The language of the format. Can be null if unknown. + *

+ * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by + * ISO 639-1. + */ + public final String language; + /** * The average bandwidth in bytes per second. * @@ -90,6 +98,21 @@ public class Format { */ public Format(String id, String mimeType, int width, int height, int numChannels, int audioSamplingRate, int bitrate) { + this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, null); + } + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 for non-video formats. + * @param height The height of the video in pixels, or -1 for non-video formats. + * @param numChannels The number of audio channels, or -1 for non-audio formats. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats. + * @param bitrate The average bandwidth of the format in bits per second. + * @param language The language of the format. + */ + public Format(String id, String mimeType, int width, int height, int numChannels, + int audioSamplingRate, int bitrate, String language) { this.id = Assertions.checkNotNull(id); this.mimeType = mimeType; this.width = width; @@ -97,6 +120,7 @@ public class Format { this.numChannels = numChannels; this.audioSamplingRate = audioSamplingRate; this.bitrate = bitrate; + this.language = language; this.bandwidth = bitrate / 8; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java index 1a87b9a142..d64ca8a262 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java @@ -164,7 +164,7 @@ public interface FormatEvaluator { */ public static class AdaptiveEvaluator implements FormatEvaluator { - public static final int DEFAULT_MAX_INITIAL_BYTE_RATE = 100000; + public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; @@ -173,7 +173,7 @@ public interface FormatEvaluator { private final BandwidthMeter bandwidthMeter; - private final int maxInitialByteRate; + private final int maxInitialBitrate; private final long minDurationForQualityIncreaseUs; private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; @@ -183,7 +183,7 @@ public interface FormatEvaluator { * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) { - this (bandwidthMeter, DEFAULT_MAX_INITIAL_BYTE_RATE, + this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); @@ -191,7 +191,7 @@ public interface FormatEvaluator { /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialByteRate The maximum bandwidth in bytes per second that should be assumed + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed * when bandwidthMeter cannot provide an estimate due to playback having only just started. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for * the evaluator to consider switching to a higher quality format. @@ -206,13 +206,13 @@ public interface FormatEvaluator { * for inaccuracies in the bandwidth estimator. */ public AdaptiveEvaluator(BandwidthMeter bandwidthMeter, - int maxInitialByteRate, + int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { this.bandwidthMeter = bandwidthMeter; - this.maxInitialByteRate = maxInitialByteRate; + this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; @@ -235,7 +235,7 @@ public interface FormatEvaluator { long bufferedDurationUs = queue.isEmpty() ? 0 : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; Format current = evaluation.format; - Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate()); + Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate()); boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate; boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; if (isHigher) { @@ -276,11 +276,11 @@ public interface FormatEvaluator { /** * Compute the ideal format ignoring buffer health. */ - protected Format determineIdealFormat(Format[] formats, long bandwidthEstimate) { - long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate); + protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) { + long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate); for (int i = 0; i < formats.length; i++) { Format format = formats[i]; - if ((format.bitrate / 8) <= effectiveBandwidth) { + if (format.bitrate <= effectiveBitrate) { return format; } } @@ -291,9 +291,9 @@ public interface FormatEvaluator { /** * Apply overhead factor, or default value in absence of estimate. */ - protected long computeEffectiveBandwidthEstimate(long bandwidthEstimate) { - return bandwidthEstimate == BandwidthMeter.NO_ESTIMATE - ? maxInitialByteRate : (long) (bandwidthEstimate * bandwidthFraction); + protected long computeEffectiveBitrateEstimate(long bitrateEstimate) { + return bitrateEstimate == BandwidthMeter.NO_ESTIMATE + ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java index 51ebac65c2..e03a529d8c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java @@ -99,6 +99,14 @@ public abstract class MediaChunk extends Chunk { */ public abstract boolean prepare() throws ParserException; + /** + * Returns whether the next sample is available. + * + * @return True if the next sample is available for reading. False otherwise. + * @throws ParserException + */ + public abstract boolean sampleAvailable() throws ParserException; + /** * Reads the next media sample from the chunk. *

diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java index 8aaee879e4..01033e73f6 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java @@ -103,12 +103,19 @@ public final class Mp4MediaChunk extends MediaChunk { return prepared; } + @Override + public boolean sampleAvailable() throws ParserException { + NonBlockingInputStream inputStream = getNonBlockingInputStream(); + int result = extractor.read(inputStream, null); + return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0; + } + @Override public boolean read(SampleHolder holder) throws ParserException { NonBlockingInputStream inputStream = getNonBlockingInputStream(); Assertions.checkState(inputStream != null); int result = extractor.read(inputStream, holder); - boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE_FULL) != 0; + boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0; if (sampleRead) { holder.timeUs -= sampleOffsetUs; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java index 6fa2f08962..dfe0d71584 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java @@ -82,11 +82,16 @@ public class SingleSampleMediaChunk extends MediaChunk { return true; } + @Override + public boolean sampleAvailable() { + return isLoadFinished() && !isReadFinished(); + } + @Override public boolean read(SampleHolder holder) { NonBlockingInputStream inputStream = getNonBlockingInputStream(); Assertions.checkState(inputStream != null); - if (!isLoadFinished()) { + if (!sampleAvailable()) { return false; } int bytesLoaded = (int) bytesLoaded(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java index f7ca26244a..4769da9772 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.parser.webm.WebmExtractor; import com.google.android.exoplayer.upstream.DataSource; @@ -69,11 +70,19 @@ public final class WebmMediaChunk extends MediaChunk { return true; } + @Override + public boolean sampleAvailable() throws ParserException { + NonBlockingInputStream inputStream = getNonBlockingInputStream(); + int result = extractor.read(inputStream, null); + return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0; + } + @Override public boolean read(SampleHolder holder) { NonBlockingInputStream inputStream = getNonBlockingInputStream(); Assertions.checkState(inputStream != null); - return extractor.read(inputStream, holder); + int result = extractor.read(inputStream, holder); + return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java index e76152859f..0013df15a4 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java @@ -146,7 +146,7 @@ public class DashMp4ChunkSource implements ChunkSource { RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; - if (extractor.getTrack() == null) { + if (extractor.getFormat() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { @@ -199,10 +199,10 @@ public class DashMp4ChunkSource implements ChunkSource { if (initializationUri != null) { // It's common for initialization and index data to be stored adjacently. Attempt to merge // the two requests together to request both at once. - expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV; + expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT; requestUri = initializationUri.attemptMerge(indexUri); if (requestUri != null) { - expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; + expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX; indexAnchor = indexUri.start + indexUri.length; } else { requestUri = initializationUri; @@ -210,7 +210,7 @@ public class DashMp4ChunkSource implements ChunkSource { } else { requestUri = indexUri; indexAnchor = indexUri.start + indexUri.length; - expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; + expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX; } DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, representation.getCacheKey()); @@ -256,9 +256,9 @@ public class DashMp4ChunkSource implements ChunkSource { throw new ParserException("Invalid extractor result. Expected " + expectedExtractorResult + ", got " + result); } - if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) { + if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) { segmentIndexes.put(format.id, - new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor)); + new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java index 133b4879ac..2f01a38120 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java @@ -56,21 +56,26 @@ public class DashWebmChunkSource implements ChunkSource { private final Format[] formats; private final HashMap representations; - private final HashMap extractors; + private final HashMap extractors; private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; + /** + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param evaluator Selects from the available formats. + * @param representations The representations to be considered by the source. + */ public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { this.dataSource = dataSource; this.evaluator = evaluator; this.formats = new Format[representations.length]; - this.extractors = new HashMap(); + this.extractors = new HashMap(); this.segmentIndexes = new HashMap(); this.representations = new HashMap(); - this.trackInfo = new TrackInfo( - representations[0].format.mimeType, representations[0].periodDurationMs * 1000); + this.trackInfo = new TrackInfo(representations[0].format.mimeType, + representations[0].periodDurationMs * 1000); this.evaluation = new Evaluation(); int maxWidth = 0; int maxHeight = 0; @@ -109,7 +114,7 @@ public class DashWebmChunkSource implements ChunkSource { @Override public void disable(List queue) { - evaluator.disable(); + evaluator.disable(); } @Override @@ -140,13 +145,18 @@ public class DashWebmChunkSource implements ChunkSource { Representation selectedRepresentation = representations.get(selectedFormat.id); WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); - if (!extractor.isPrepared()) { - // TODO: This code forces cues to exist and to immediately follow the initialization - // data. Webm extractor should be generalized to allow cues to be optional. See [redacted]. - RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge( - selectedRepresentation.getIndexUri()); - Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation, - extractor, dataSource, evaluation.trigger); + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; + if (extractor.getFormat() == null) { + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { + pendingIndexUri = selectedRepresentation.getIndexUri(); + } + if (pendingInitializationUri != null || pendingIndexUri != null) { + // We have initialization and/or index requests to make. + Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, + selectedRepresentation, extractor, dataSource, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; @@ -181,12 +191,29 @@ public class DashWebmChunkSource implements ChunkSource { // Do nothing. } - private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation, - WebmExtractor extractor, DataSource dataSource, int trigger) { - DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start, - initializationUri.length, representation.getCacheKey()); + private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, + Representation representation, WebmExtractor extractor, DataSource dataSource, + int trigger) { + int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM; + RangedUri requestUri; + if (initializationUri != null) { + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT; + requestUri = initializationUri.attemptMerge(indexUri); + if (requestUri != null) { + expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX; + } else { + requestUri = initializationUri; + } + } else { + requestUri = indexUri; + expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX; + } + DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, + representation.getCacheKey()); return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format, - extractor); + extractor, expectedExtractorResult); } private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, @@ -206,22 +233,27 @@ public class DashWebmChunkSource implements ChunkSource { private class InitializationWebmLoadable extends Chunk { private final WebmExtractor extractor; + private final int expectedExtractorResult; private final Uri uri; public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, - Format format, WebmExtractor extractor) { + Format format, WebmExtractor extractor, int expectedExtractorResult) { super(dataSource, dataSpec, format, trigger); this.extractor = extractor; + this.expectedExtractorResult = expectedExtractorResult; this.uri = dataSpec.uri; } @Override protected void consumeStream(NonBlockingInputStream stream) throws IOException { - extractor.read(stream, null); - if (!extractor.isPrepared()) { - throw new ParserException("Invalid initialization data"); + int result = extractor.read(stream, null); + if (result != expectedExtractorResult) { + throw new ParserException("Invalid extractor result. Expected " + + expectedExtractorResult + ", got " + result); + } + if ((result & WebmExtractor.RESULT_READ_INDEX) != 0) { + segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0)); } - segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 3bf9666006..2bd53d998b 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -140,6 +140,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { throws XmlPullParserException, IOException { String mimeType = xpp.getAttributeValue(null, "mimeType"); + String language = xpp.getAttributeValue(null, "lang"); int contentType = parseAdaptationSetTypeFromMimeType(mimeType); int id = -1; @@ -160,7 +161,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); } else if (isStartTag(xpp, "Representation")) { Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs, - periodDurationMs, mimeType, segmentBase); + periodDurationMs, mimeType, language, segmentBase); contentType = checkAdaptationSetTypeConsistency(contentType, parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); representations.add(representation); @@ -230,8 +231,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { // Representation parsing. private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, - long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase) - throws XmlPullParserException, IOException { + long periodStartMs, long periodDurationMs, String mimeType, String language, + SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); @@ -257,7 +258,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { } while (!isEndTag(xpp, "Representation")); Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, - bandwidth); + bandwidth, language); return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format, segmentBase); } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java index 716ca458bf..fbdccc2d67 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer.parser.mp4; import java.util.ArrayList; -import java.util.List; /* package */ abstract class Atom { @@ -24,7 +23,6 @@ import java.util.List; public static final int TYPE_avc3 = 0x61766333; public static final int TYPE_esds = 0x65736473; public static final int TYPE_mdat = 0x6D646174; - public static final int TYPE_mfhd = 0x6D666864; public static final int TYPE_mp4a = 0x6D703461; public static final int TYPE_tfdt = 0x74666474; public static final int TYPE_tfhd = 0x74666864; @@ -54,6 +52,7 @@ import java.util.List; public static final int TYPE_frma = 0x66726D61; public static final int TYPE_saiz = 0x7361697A; public static final int TYPE_uuid = 0x75756964; + public static final int TYPE_senc = 0x73656E63; public final int type; @@ -63,17 +62,13 @@ import java.util.List; public final static class LeafAtom extends Atom { - private final ParsableByteArray data; + public final ParsableByteArray data; public LeafAtom(int type, ParsableByteArray data) { super(type); this.data = data; } - public ParsableByteArray getData() { - return data; - } - } public final static class ContainerAtom extends Atom { @@ -90,7 +85,8 @@ import java.util.List; } public LeafAtom getLeafAtomOfType(int type) { - for (int i = 0; i < children.size(); i++) { + int childrenSize = children.size(); + for (int i = 0; i < childrenSize; i++) { Atom atom = children.get(i); if (atom.type == type) { return (LeafAtom) atom; @@ -100,7 +96,8 @@ import java.util.List; } public ContainerAtom getContainerAtomOfType(int type) { - for (int i = 0; i < children.size(); i++) { + int childrenSize = children.size(); + for (int i = 0; i < childrenSize; i++) { Atom atom = children.get(i); if (atom.type == type) { return (ContainerAtom) atom; @@ -109,10 +106,6 @@ import java.util.List; return null; } - public List getChildren() { - return children; - } - } } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java index e441670cf6..851c4925b6 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java @@ -27,7 +27,7 @@ import java.util.List; /** * Provides static utility methods for manipulating various types of codec specific data. */ -public class CodecSpecificDataUtil { +public final class CodecSpecificDataUtil { private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 16e1788943..936bd565d0 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -59,7 +59,7 @@ public final class FragmentedMp4Extractor { public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; /** - * An attempt to read from the input stream returned 0 bytes of data. + * An attempt to read from the input stream returned insufficient data. */ public static final int RESULT_NEED_MORE_DATA = 1; /** @@ -69,27 +69,23 @@ public final class FragmentedMp4Extractor { /** * A media sample was read. */ - public static final int RESULT_READ_SAMPLE_FULL = 4; + public static final int RESULT_READ_SAMPLE = 4; /** - * A media sample was partially read. + * A moov atom was read. The parsed data can be read using {@link #getFormat()} and + * {@link #getPsshInfo}. */ - public static final int RESULT_READ_SAMPLE_PARTIAL = 8; + public static final int RESULT_READ_INIT = 8; /** - * A moov atom was read. The parsed data can be read using {@link #getTrack()}, - * {@link #getFormat()} and {@link #getPsshInfo}. + * A sidx atom was read. The parsed data can be read using {@link #getIndex()}. */ - public static final int RESULT_READ_MOOV = 16; - /** - * A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}. - */ - public static final int RESULT_READ_SIDX = 32; + public static final int RESULT_READ_INDEX = 16; /** * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. */ - public static final int RESULT_NEED_SAMPLE_HOLDER = 64; + public static final int RESULT_NEED_SAMPLE_HOLDER = 32; private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM - | RESULT_READ_SAMPLE_FULL | RESULT_NEED_SAMPLE_HOLDER; + | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; @@ -97,9 +93,8 @@ public final class FragmentedMp4Extractor { // Parser states private static final int STATE_READING_ATOM_HEADER = 0; private static final int STATE_READING_ATOM_PAYLOAD = 1; - private static final int STATE_READING_CENC_AUXILIARY_DATA = 2; - private static final int STATE_READING_SAMPLE_START = 3; - private static final int STATE_READING_SAMPLE_INCREMENTAL = 4; + private static final int STATE_READING_ENCRYPTION_DATA = 2; + private static final int STATE_READING_SAMPLE = 3; // Atom data offsets private static final int ATOM_HEADER_SIZE = 8; @@ -115,7 +110,6 @@ public final class FragmentedMp4Extractor { parsedAtoms.add(Atom.TYPE_hdlr); parsedAtoms.add(Atom.TYPE_mdat); parsedAtoms.add(Atom.TYPE_mdhd); - parsedAtoms.add(Atom.TYPE_mfhd); parsedAtoms.add(Atom.TYPE_moof); parsedAtoms.add(Atom.TYPE_moov); parsedAtoms.add(Atom.TYPE_mp4a); @@ -135,6 +129,7 @@ public final class FragmentedMp4Extractor { parsedAtoms.add(Atom.TYPE_pssh); parsedAtoms.add(Atom.TYPE_saiz); parsedAtoms.add(Atom.TYPE_uuid); + parsedAtoms.add(Atom.TYPE_senc); PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms); } @@ -158,8 +153,10 @@ public final class FragmentedMp4Extractor { // Parser state private final ParsableByteArray atomHeader; + private final byte[] extendedTypeScratch; private final Stack containerAtoms; private final Stack containerAtomEndPoints; + private final TrackFragment fragmentRun; private int parserState; private int atomBytesRead; @@ -167,9 +164,6 @@ public final class FragmentedMp4Extractor { private int atomType; private int atomSize; private ParsableByteArray atomData; - private ParsableByteArray cencAuxiliaryData; - private int cencAuxiliaryBytesRead; - private int sampleBytesRead; private int pendingSeekTimeMs; private int sampleIndex; @@ -182,9 +176,6 @@ public final class FragmentedMp4Extractor { private Track track; private DefaultSampleValues extendsDefaults; - // Data parsed from the most recent moof atom - private TrackFragment fragmentRun; - public FragmentedMp4Extractor() { this(0); } @@ -197,8 +188,10 @@ public final class FragmentedMp4Extractor { this.workaroundFlags = workaroundFlags; parserState = STATE_READING_ATOM_HEADER; atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE); + extendedTypeScratch = new byte[16]; containerAtoms = new Stack(); containerAtomEndPoints = new Stack(); + fragmentRun = new TrackFragment(); psshData = new HashMap(); } @@ -207,7 +200,7 @@ public final class FragmentedMp4Extractor { * * @return The segment index, or null if a SIDX atom has yet to be parsed. */ - public SegmentIndex getSegmentIndex() { + public SegmentIndex getIndex() { return segmentIndex; } @@ -245,17 +238,7 @@ public final class FragmentedMp4Extractor { } /** - * Returns the track information parsed from the stream. - * - * @return The track, or null if a MOOV atom has yet to be parsed. - */ - public Track getTrack() { - return track; - } - - /** - * Sideloads track information into the extractor, so that it can be read through - * {@link #getTrack()}. + * Sideloads track information into the extractor. * * @param track The track to sideload. */ @@ -270,10 +253,6 @@ public final class FragmentedMp4Extractor { * The read terminates if the end of the input stream is reached, if an attempt to read from the * input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate * both the reason for termination and data that was parsed during the read. - *

- * If the returned flags include {@link #RESULT_READ_SAMPLE_PARTIAL} then the sample has been - * partially read into {@code out}. Hence the same {@link SampleHolder} instance must be passed - * in subsequent calls until the whole sample has been read. * * @param inputStream The input stream from which data should be read. * @param out A {@link SampleHolder} into which the next sample should be read. If null then @@ -293,8 +272,8 @@ public final class FragmentedMp4Extractor { case STATE_READING_ATOM_PAYLOAD: results |= readAtomPayload(inputStream); break; - case STATE_READING_CENC_AUXILIARY_DATA: - results |= readCencAuxiliaryData(inputStream); + case STATE_READING_ENCRYPTION_DATA: + results |= readEncryptionData(inputStream); break; default: results |= readOrSkipSample(inputStream, out); @@ -350,19 +329,13 @@ public final class FragmentedMp4Extractor { rootAtomBytesRead = 0; } break; - case STATE_READING_CENC_AUXILIARY_DATA: - cencAuxiliaryBytesRead = 0; - break; - case STATE_READING_SAMPLE_START: - sampleBytesRead = 0; - break; } parserState = state; } private int readAtomHeader(NonBlockingInputStream inputStream) { int remainingBytes = ATOM_HEADER_SIZE - atomBytesRead; - int bytesRead = inputStream.read(atomHeader.getData(), atomBytesRead, remainingBytes); + int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes); if (bytesRead == -1) { return RESULT_END_OF_STREAM; } @@ -377,13 +350,10 @@ public final class FragmentedMp4Extractor { atomType = atomHeader.readInt(); if (atomType == Atom.TYPE_mdat) { - int cencAuxSize = fragmentRun.auxiliarySampleInfoTotalSize; - if (cencAuxSize > 0) { - cencAuxiliaryData = new ParsableByteArray(cencAuxSize); - enterState(STATE_READING_CENC_AUXILIARY_DATA); + if (fragmentRun.sampleEncryptionDataNeedsFill) { + enterState(STATE_READING_ENCRYPTION_DATA); } else { - cencAuxiliaryData = null; - enterState(STATE_READING_SAMPLE_START); + enterState(STATE_READING_SAMPLE); } return 0; } @@ -395,7 +365,7 @@ public final class FragmentedMp4Extractor { containerAtomEndPoints.add(rootAtomBytesRead + atomSize - ATOM_HEADER_SIZE); } else { atomData = new ParsableByteArray(atomSize); - System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, ATOM_HEADER_SIZE); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, ATOM_HEADER_SIZE); enterState(STATE_READING_ATOM_PAYLOAD); } } else { @@ -409,7 +379,7 @@ public final class FragmentedMp4Extractor { private int readAtomPayload(NonBlockingInputStream inputStream) { int bytesRead; if (atomData != null) { - bytesRead = inputStream.read(atomData.getData(), atomBytesRead, atomSize - atomBytesRead); + bytesRead = inputStream.read(atomData.data, atomBytesRead, atomSize - atomBytesRead); } else { bytesRead = inputStream.skip(atomSize - atomBytesRead); } @@ -441,8 +411,8 @@ public final class FragmentedMp4Extractor { if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(leaf); } else if (leaf.type == Atom.TYPE_sidx) { - segmentIndex = parseSidx(leaf.getData()); - return RESULT_READ_SIDX; + segmentIndex = parseSidx(leaf.data); + return RESULT_READ_INDEX; } return 0; } @@ -450,7 +420,7 @@ public final class FragmentedMp4Extractor { private int onContainerAtomRead(ContainerAtom container) { if (container.type == Atom.TYPE_moov) { onMoovContainerAtomRead(container); - return RESULT_READ_MOOV; + return RESULT_READ_INIT; } else if (container.type == Atom.TYPE_moof) { onMoofContainerAtomRead(container); } else if (!containerAtoms.isEmpty()) { @@ -460,11 +430,12 @@ public final class FragmentedMp4Extractor { } private void onMoovContainerAtomRead(ContainerAtom moov) { - List moovChildren = moov.getChildren(); - for (int i = 0; i < moovChildren.size(); i++) { + List moovChildren = moov.children; + int moovChildrenSize = moovChildren.size(); + for (int i = 0; i < moovChildrenSize; i++) { Atom child = moovChildren.get(i); if (child.type == Atom.TYPE_pssh) { - ParsableByteArray psshAtom = ((LeafAtom) child).getData(); + ParsableByteArray psshAtom = ((LeafAtom) child).data; psshAtom.setPosition(FULL_ATOM_HEADER_SIZE); UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong()); int dataSize = psshAtom.readInt(); @@ -474,13 +445,13 @@ public final class FragmentedMp4Extractor { } } ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); - extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).getData()); + extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data); track = parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak)); } private void onMoofContainerAtomRead(ContainerAtom moof) { - fragmentRun = new TrackFragment(); - parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags); + fragmentRun.reset(); + parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch); sampleIndex = 0; lastSyncSampleIndex = 0; pendingSeekSyncSampleIndex = 0; @@ -514,21 +485,21 @@ public final class FragmentedMp4Extractor { */ private static Track parseTrak(ContainerAtom trak) { ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); - int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).getData()); + int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO); - Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).getData()); + Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); int id = header.first; // TODO: This value should be used to set a duration field on the Track object // instantiated below, however we've found examples where the value is 0. Revisit whether we // should set it anyway (and just have it be wrong for bad media streams). // long duration = header.second; - long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).getData()); + long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) .getContainerAtomOfType(Atom.TYPE_stbl); Pair sampleDescriptions = - parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).getData()); + parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data); return new Track(id, trackType, timescale, sampleDescriptions.first, sampleDescriptions.second); } @@ -654,7 +625,7 @@ public final class FragmentedMp4Extractor { if (childAtomType == Atom.TYPE_esds) { initializationData = parseEsdsFromParent(parent, childStartPosition); // TODO: Do we really need to do this? See [redacted] - // Update sampleRate and sampleRate from the AudioSpecificConfig initialization data. + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. Pair audioSpecificConfig = CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); sampleRate = audioSpecificConfig.first; @@ -697,7 +668,7 @@ public final class FragmentedMp4Extractor { int length = atom.readUnsignedShort(); int offset = atom.getPosition(); atom.skip(length); - return CodecSpecificDataUtil.buildNalUnit(atom.getData(), offset, length); + return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); } private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, @@ -775,7 +746,7 @@ public final class FragmentedMp4Extractor { parent.skip(13); // Start of AudioSpecificConfig (defined in 14496-3) - parent.skip(1); // AudioSpecificConfig tag + parent.skip(1); // AudioSpecificConfig tag varIntByte = parent.readUnsignedByte(); int varInt = varIntByte & 0x7F; while (varIntByte > 127) { @@ -789,49 +760,47 @@ public final class FragmentedMp4Extractor { } private static void parseMoof(Track track, DefaultSampleValues extendsDefaults, - ContainerAtom moof, TrackFragment out, int workaroundFlags) { - // TODO: Consider checking that the sequence number returned by parseMfhd is as expected. - parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData()); + ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) { parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf), - out, workaroundFlags); - } - - /** - * Parses an mfhd atom (defined in 14496-12). - * - * @param mfhd The mfhd atom to parse. - * @return The sequence number of the fragment. - */ - private static int parseMfhd(ParsableByteArray mfhd) { - mfhd.setPosition(FULL_ATOM_HEADER_SIZE); - return mfhd.readUnsignedIntToInt(); + out, workaroundFlags, extendedTypeScratch); } /** * Parses a traf atom (defined in 14496-12). */ private static void parseTraf(Track track, DefaultSampleValues extendsDefaults, - ContainerAtom traf, TrackFragment out, int workaroundFlags) { - LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); - if (saiz != null) { - parseSaiz(saiz.getData(), out); - } + ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) { LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); - long decodeTime = tfdtAtom == null ? 0 - : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).getData()); + long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.getData()); - out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex); + DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data); + out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex; LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); - parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out); + parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out); + + TrackEncryptionBox trackEncryptionBox = + track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex]; + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + if (saiz != null) { + parseSaiz(trackEncryptionBox, saiz.data, out); + } + + LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + if (senc != null) { + parseSenc(senc.data, out); + } + LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid); if (uuid != null) { - parseUuid(uuid.getData(), out); + parseUuid(uuid.data, out, extendedTypeScratch); } } - private static void parseSaiz(ParsableByteArray saiz, TrackFragment out) { + private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, + TrackFragment out) { + int vectorSize = encryptionBox.initializationVectorSize; saiz.setPosition(ATOM_HEADER_SIZE); int fullAtom = saiz.readInt(); int flags = parseFullAtomFlags(fullAtom); @@ -839,21 +808,26 @@ public final class FragmentedMp4Extractor { saiz.skip(8); } int defaultSampleInfoSize = saiz.readUnsignedByte(); + int sampleCount = saiz.readUnsignedIntToInt(); + if (sampleCount != out.length) { + throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length); + } + int totalSize = 0; - int[] sampleInfoSizes = new int[sampleCount]; if (defaultSampleInfoSize == 0) { + boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable; for (int i = 0; i < sampleCount; i++) { - sampleInfoSizes[i] = saiz.readUnsignedByte(); - totalSize += sampleInfoSizes[i]; + int sampleInfoSize = saiz.readUnsignedByte(); + totalSize += sampleInfoSize; + sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize; } } else { - for (int i = 0; i < sampleCount; i++) { - sampleInfoSizes[i] = defaultSampleInfoSize; - totalSize += defaultSampleInfoSize; - } + boolean subsampleEncryption = defaultSampleInfoSize > vectorSize; + totalSize += defaultSampleInfoSize * sampleCount; + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); } - out.setAuxiliarySampleInfoTables(totalSize, sampleInfoSizes); + out.initEncryptionData(totalSize); } /** @@ -912,10 +886,9 @@ public final class FragmentedMp4Extractor { long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) { trun.setPosition(ATOM_HEADER_SIZE); int fullAtom = trun.readInt(); - int version = parseFullAtomVersion(fullAtom); int flags = parseFullAtomFlags(fullAtom); - int numberOfEntries = trun.readUnsignedIntToInt(); + int sampleCount = trun.readUnsignedIntToInt(); if ((flags & 0x01 /* data_offset_present */) != 0) { trun.skip(4); } @@ -932,17 +905,18 @@ public final class FragmentedMp4Extractor { boolean sampleCompositionTimeOffsetsPresent = (flags & 0x800 /* sample_composition_time_offsets_present */) != 0; - int[] sampleSizeTable = new int[numberOfEntries]; - int[] sampleDecodingTimeTable = new int[numberOfEntries]; - int[] sampleCompositionTimeOffsetTable = new int[numberOfEntries]; - boolean[] sampleIsSyncFrameTable = new boolean[numberOfEntries]; + out.initTables(sampleCount); + int[] sampleSizeTable = out.sampleSizeTable; + int[] sampleDecodingTimeTable = out.sampleDecodingTimeTable; + int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable; + boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable; long timescale = track.timescale; long cumulativeTime = decodeTime; boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO && ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) == WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); - for (int i = 0; i < numberOfEntries; i++) { + for (int i = 0; i < sampleCount; i++) { // Use trun values if present, otherwise tfhd, otherwise trex. int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.duration; @@ -950,48 +924,47 @@ public final class FragmentedMp4Extractor { int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; if (sampleCompositionTimeOffsetsPresent) { - int sampleOffset; - if (version == 0) { - // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in - // version 0 trun boxes, however a significant number of streams violate the spec and use - // signed integers instead. It's safe to always parse sample offsets as signed integers - // here, because unsigned integers will still be parsed correctly (unless their top bit is - // set, which is never true in practice because sample offsets are always small). - sampleOffset = trun.readInt(); - } else { - sampleOffset = trun.readInt(); - } + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in + // version 0 trun boxes, however a significant number of streams violate the spec and use + // signed integers instead. It's safe to always parse sample offsets as signed integers + // here, because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + int sampleOffset = trun.readInt(); sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale); + } else { + sampleCompositionTimeOffsetTable[i] = 0; } sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale); sampleSizeTable[i] = sampleSize; - boolean isSync = ((sampleFlags >> 16) & 0x1) == 0; - if (workaroundEveryVideoFrameIsSyncFrame && i != 0) { - isSync = false; - } - if (isSync) { - sampleIsSyncFrameTable[i] = true; - } + sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 + && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); cumulativeTime += sampleDuration; } - - out.setSampleTables(sampleSizeTable, sampleDecodingTimeTable, sampleCompositionTimeOffsetTable, - sampleIsSyncFrameTable); } - private static void parseUuid(ParsableByteArray uuid, TrackFragment out) { + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, + byte[] extendedTypeScratch) { uuid.setPosition(ATOM_HEADER_SIZE); - byte[] extendedType = new byte[16]; - uuid.readBytes(extendedType, 0, 16); + uuid.readBytes(extendedTypeScratch, 0, 16); // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. - if (!Arrays.equals(extendedType, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) { + if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) { return; } - // See "Portable encoding of audio-video objects: The Protected Interoperable File Format - // (PIFF), John A. Bocharov et al, Section 5.3.2.1." - int fullAtom = uuid.readInt(); + // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of + // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al, + // Section 5.3.2.1." + parseSenc(uuid, 16, out); + } + + private static void parseSenc(ParsableByteArray senc, TrackFragment out) { + parseSenc(senc, 0, out); + } + + private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) { + senc.setPosition(ATOM_HEADER_SIZE + offset); + int fullAtom = senc.readInt(); int flags = parseFullAtomFlags(fullAtom); if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { @@ -1000,15 +973,14 @@ public final class FragmentedMp4Extractor { } boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; - int numberOfEntries = uuid.readUnsignedIntToInt(); - if (numberOfEntries != out.length) { - throw new IllegalStateException("Length mismatch: " + numberOfEntries + ", " + out.length); + int sampleCount = senc.readUnsignedIntToInt(); + if (sampleCount != out.length) { + throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length); } - int sampleEncryptionDataLength = uuid.length() - uuid.getPosition(); - ParsableByteArray sampleEncryptionData = new ParsableByteArray(sampleEncryptionDataLength); - uuid.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionData.length()); - out.setSmoothStreamingSampleEncryptionData(sampleEncryptionData, subsampleEncryption); + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + out.initEncryptionData(senc.length() - senc.getPosition()); + out.fillEncryptionData(senc); } /** @@ -1067,18 +1039,12 @@ public final class FragmentedMp4Extractor { return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs); } - private int readCencAuxiliaryData(NonBlockingInputStream inputStream) { - int length = cencAuxiliaryData.length(); - int bytesRead = inputStream.read(cencAuxiliaryData.getData(), cencAuxiliaryBytesRead, - length - cencAuxiliaryBytesRead); - if (bytesRead == -1) { - return RESULT_END_OF_STREAM; - } - cencAuxiliaryBytesRead += bytesRead; - if (cencAuxiliaryBytesRead < length) { + private int readEncryptionData(NonBlockingInputStream inputStream) { + boolean success = fragmentRun.fillEncryptionData(inputStream); + if (!success) { return RESULT_NEED_MORE_DATA; } - enterState(STATE_READING_SAMPLE_START); + enterState(STATE_READING_SAMPLE); return 0; } @@ -1105,89 +1071,62 @@ public final class FragmentedMp4Extractor { enterState(STATE_READING_ATOM_HEADER); return 0; } - if (sampleIndex < pendingSeekSyncSampleIndex) { - return skipSample(inputStream); + int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; + if (inputStream.getAvailableByteCount() < sampleSize) { + return RESULT_NEED_MORE_DATA; } - return readSample(inputStream, out); + if (sampleIndex < pendingSeekSyncSampleIndex) { + return skipSample(inputStream, sampleSize); + } + return readSample(inputStream, sampleSize, out); } - private int skipSample(NonBlockingInputStream inputStream) { - if (parserState == STATE_READING_SAMPLE_START) { - ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData - : fragmentRun.smoothStreamingSampleEncryptionData; - if (sampleEncryptionData != null) { - TrackEncryptionBox encryptionBox = - track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; - int vectorSize = encryptionBox.initializationVectorSize; - boolean subsampleEncryption = cencAuxiliaryData != null - ? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize - : fragmentRun.smoothStreamingUsesSubsampleEncryption; - sampleEncryptionData.skip(vectorSize); - int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1; - if (subsampleEncryption) { - sampleEncryptionData.skip((2 + 4) * subsampleCount); - } + private int skipSample(NonBlockingInputStream inputStream, int sampleSize) { + if (fragmentRun.definesEncryptionData) { + ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; + TrackEncryptionBox encryptionBox = + track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; + int vectorSize = encryptionBox.initializationVectorSize; + boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex]; + sampleEncryptionData.skip(vectorSize); + int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1; + if (subsampleEncryption) { + sampleEncryptionData.skip((2 + 4) * subsampleCount); } } - int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; - int bytesRead = inputStream.skip(sampleSize - sampleBytesRead); - if (bytesRead == -1) { - return RESULT_END_OF_STREAM; - } - sampleBytesRead += bytesRead; - if (sampleSize != sampleBytesRead) { - enterState(STATE_READING_SAMPLE_INCREMENTAL); - return RESULT_NEED_MORE_DATA; - } + inputStream.skip(sampleSize); + sampleIndex++; - enterState(STATE_READING_SAMPLE_START); + enterState(STATE_READING_SAMPLE); return 0; } @SuppressLint("InlinedApi") - private int readSample(NonBlockingInputStream inputStream, SampleHolder out) { + private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) { if (out == null) { return RESULT_NEED_SAMPLE_HOLDER; } - int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; ByteBuffer outputData = out.data; - if (parserState == STATE_READING_SAMPLE_START) { - out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; - out.flags = 0; - if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { - out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; - lastSyncSampleIndex = sampleIndex; - } - if (out.allowDataBufferReplacement - && (out.data == null || out.data.capacity() < sampleSize)) { - outputData = ByteBuffer.allocate(sampleSize); - out.data = outputData; - } - ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData - : fragmentRun.smoothStreamingSampleEncryptionData; - if (sampleEncryptionData != null) { - readSampleEncryptionData(sampleEncryptionData, out); - } + out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; + out.flags = 0; + if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { + out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; + lastSyncSampleIndex = sampleIndex; + } + if (out.allowDataBufferReplacement && (out.data == null || out.data.capacity() < sampleSize)) { + outputData = ByteBuffer.allocate(sampleSize); + out.data = outputData; + } + if (fragmentRun.definesEncryptionData) { + readSampleEncryptionData(fragmentRun.sampleEncryptionData, out); } - int bytesRead; if (outputData == null) { - bytesRead = inputStream.skip(sampleSize - sampleBytesRead); + inputStream.skip(sampleSize); + out.size = 0; } else { - bytesRead = inputStream.read(outputData, sampleSize - sampleBytesRead); - } - if (bytesRead == -1) { - return RESULT_END_OF_STREAM; - } - sampleBytesRead += bytesRead; - - if (sampleSize != sampleBytesRead) { - enterState(STATE_READING_SAMPLE_INCREMENTAL); - return RESULT_NEED_MORE_DATA | RESULT_READ_SAMPLE_PARTIAL; - } - - if (outputData != null) { + inputStream.read(outputData, sampleSize); if (track.type == Track.TYPE_VIDEO) { // The mp4 file contains length-prefixed NAL units, but the decoder wants start code // delimited content. Replace length prefixes with start codes. @@ -1203,13 +1142,11 @@ public final class FragmentedMp4Extractor { outputData.position(sampleOffset + sampleSize); } out.size = sampleSize; - } else { - out.size = 0; } sampleIndex++; - enterState(STATE_READING_SAMPLE_START); - return RESULT_READ_SAMPLE_FULL; + enterState(STATE_READING_SAMPLE); + return RESULT_READ_SAMPLE; } @SuppressLint("InlinedApi") @@ -1219,9 +1156,7 @@ public final class FragmentedMp4Extractor { byte[] keyId = encryptionBox.keyId; boolean isEncrypted = encryptionBox.isEncrypted; int vectorSize = encryptionBox.initializationVectorSize; - boolean subsampleEncryption = cencAuxiliaryData != null - ? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize - : fragmentRun.smoothStreamingUsesSubsampleEncryption; + boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex]; byte[] vector = out.cryptoInfo.iv; if (vector == null || vector.length != 16) { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java index 13027f0174..bf4472d5a7 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java @@ -23,17 +23,14 @@ import java.nio.ByteBuffer; */ /* package */ final class ParsableByteArray { - private final byte[] data; + public byte[] data; + private int position; public ParsableByteArray(int length) { this.data = new byte[length]; } - public byte[] getData() { - return data; - } - public int length() { return data.length; } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java index 2cc899ef71..6a300e7230 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer.parser.mp4; /** * Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream. */ -public class TrackEncryptionBox { +public final class TrackEncryptionBox { /** * Indicates the encryption state of the samples in the sample group. diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java index 98f33968ca..e2e08225b2 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java @@ -15,48 +15,136 @@ */ package com.google.android.exoplayer.parser.mp4; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; + /** * A holder for information corresponding to a single fragment of an mp4 file. */ -/* package */ class TrackFragment { +/* package */ final class TrackFragment { public int sampleDescriptionIndex; + /** + * The number of samples contained by the fragment. + */ public int length; + /** + * The size of each sample in the run. + */ public int[] sampleSizeTable; + /** + * The decoding time of each sample in the run. + */ public int[] sampleDecodingTimeTable; + /** + * The composition time offset of each sample in the run. + */ public int[] sampleCompositionTimeOffsetTable; + /** + * Indicates which samples are sync frames. + */ public boolean[] sampleIsSyncFrameTable; + /** + * True if the fragment defines encryption data. False otherwise. + */ + public boolean definesEncryptionData; + /** + * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption. + * Undefined otherwise. + */ + public boolean[] sampleHasSubsampleEncryptionTable; + /** + * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data. + * Undefined otherwise. + */ + public int sampleEncryptionDataLength; + /** + * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined + * otherwise. + */ + public ParsableByteArray sampleEncryptionData; + /** + * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data. + */ + public boolean sampleEncryptionDataNeedsFill; - public int auxiliarySampleInfoTotalSize; - public int[] auxiliarySampleInfoSizeTable; - - public boolean smoothStreamingUsesSubsampleEncryption; - public ParsableByteArray smoothStreamingSampleEncryptionData; - - public void setSampleDescriptionIndex(int sampleDescriptionIndex) { - this.sampleDescriptionIndex = sampleDescriptionIndex; + /** + * Resets the fragment. + *

+ * The {@link #length} is set to 0, and both {@link #definesEncryptionData} and + * {@link #sampleEncryptionDataNeedsFill} is set to false. + */ + public void reset() { + length = 0; + definesEncryptionData = false; + sampleEncryptionDataNeedsFill = false; } - public void setSampleTables(int[] sampleSizeTable, int[] sampleDecodingTimeTable, - int[] sampleCompositionTimeOffsetTable, boolean[] sampleIsSyncFrameTable) { - this.sampleSizeTable = sampleSizeTable; - this.sampleDecodingTimeTable = sampleDecodingTimeTable; - this.sampleCompositionTimeOffsetTable = sampleCompositionTimeOffsetTable; - this.sampleIsSyncFrameTable = sampleIsSyncFrameTable; - this.length = sampleSizeTable.length; + /** + * Configures the fragment for the specified number of samples. + *

+ * The {@link #length} of the fragment is set to the specified sample count, and the contained + * tables are resized if necessary such that they are at least this length. + * + * @param sampleCount The number of samples in the new run. + */ + public void initTables(int sampleCount) { + length = sampleCount; + if (sampleSizeTable == null || sampleSizeTable.length < length) { + // Size the tables 25% larger than needed, so as to make future resize operations less + // likely. The choice of 25% is relatively arbitrary. + int tableSize = (sampleCount * 125) / 100; + sampleSizeTable = new int[tableSize]; + sampleDecodingTimeTable = new int[tableSize]; + sampleCompositionTimeOffsetTable = new int[tableSize]; + sampleIsSyncFrameTable = new boolean[tableSize]; + sampleHasSubsampleEncryptionTable = new boolean[tableSize]; + } } - public void setAuxiliarySampleInfoTables(int totalAuxiliarySampleInfoSize, - int[] auxiliarySampleInfoSizeTable) { - this.auxiliarySampleInfoTotalSize = totalAuxiliarySampleInfoSize; - this.auxiliarySampleInfoSizeTable = auxiliarySampleInfoSizeTable; + /** + * Configures the fragment to be one that defines encryption data of the specified length. + *

+ * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to + * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it + * is at least this length. + * + * @param length The length in bytes of the encryption data. + */ + public void initEncryptionData(int length) { + if (sampleEncryptionData == null || sampleEncryptionData.length() < length) { + sampleEncryptionData = new ParsableByteArray(length); + } + sampleEncryptionDataLength = length; + definesEncryptionData = true; + sampleEncryptionDataNeedsFill = true; } - public void setSmoothStreamingSampleEncryptionData(ParsableByteArray data, - boolean usesSubsampleEncryption) { - this.smoothStreamingSampleEncryptionData = data; - this.smoothStreamingUsesSubsampleEncryption = usesSubsampleEncryption; + /** + * Fills {@link #sampleEncryptionData} from the provided source. + * + * @param source A source from which to read the encryption data. + */ + public void fillEncryptionData(ParsableByteArray source) { + source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + /** + * Fills {@link #sampleEncryptionData} for the current run from the provided source. + * + * @param source A source from which to read the encryption data. + * @return True if the encryption data was filled. False if the source had insufficient data. + */ + public boolean fillEncryptionData(NonBlockingInputStream source) { + if (source.getAvailableByteCount() < sampleEncryptionDataLength) { + return false; + } + source.read(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + return true; } public int getSamplePresentationTime(int index) { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java index f66b83293f..55eca63de6 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java @@ -138,9 +138,8 @@ import java.util.Stack; while (true) { while (!masterElementsStack.isEmpty() && bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) { - if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) { - return READ_RESULT_CONTINUE; - } + eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId); + return READ_RESULT_CONTINUE; } if (state == STATE_BEGIN_READING) { @@ -161,12 +160,10 @@ import java.util.Stack; case TYPE_MASTER: int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize)); - if (!eventHandler.onMasterElementStart( - elementId, elementOffset, masterHeaderSize, elementContentSize)) { - prepareForNextElement(); - return READ_RESULT_CONTINUE; - } - break; + eventHandler.onMasterElementStart(elementId, elementOffset, masterHeaderSize, + elementContentSize); + prepareForNextElement(); + return READ_RESULT_CONTINUE; case TYPE_UNSIGNED_INT: if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { throw new IllegalStateException("Invalid integer size " + elementContentSize); @@ -177,11 +174,9 @@ import java.util.Stack; return intResult; } long intValue = getTempByteArrayValue((int) elementContentSize, false); - if (!eventHandler.onIntegerElement(elementId, intValue)) { - prepareForNextElement(); - return READ_RESULT_CONTINUE; - } - break; + eventHandler.onIntegerElement(elementId, intValue); + prepareForNextElement(); + return READ_RESULT_CONTINUE; case TYPE_FLOAT: if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { @@ -199,11 +194,9 @@ import java.util.Stack; } else { floatValue = Double.longBitsToDouble(valueBits); } - if (!eventHandler.onFloatElement(elementId, floatValue)) { - prepareForNextElement(); - return READ_RESULT_CONTINUE; - } - break; + eventHandler.onFloatElement(elementId, floatValue); + prepareForNextElement(); + return READ_RESULT_CONTINUE; case TYPE_STRING: if (elementContentSize > Integer.MAX_VALUE) { throw new IllegalStateException( @@ -219,11 +212,9 @@ import java.util.Stack; } String stringValue = new String(stringBytes, Charset.forName("UTF-8")); stringBytes = null; - if (!eventHandler.onStringElement(elementId, stringValue)) { - prepareForNextElement(); - return READ_RESULT_CONTINUE; - } - break; + eventHandler.onStringElement(elementId, stringValue); + prepareForNextElement(); + return READ_RESULT_CONTINUE; case TYPE_BINARY: if (elementContentSize > Integer.MAX_VALUE) { throw new IllegalStateException( @@ -233,18 +224,17 @@ import java.util.Stack; return READ_RESULT_NEED_MORE_DATA; } int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. - boolean keepGoing = eventHandler.onBinaryElement( + boolean consumed = eventHandler.onBinaryElement( elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream); - long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize; - if (expectedBytesRead != bytesRead) { - throw new IllegalStateException("Incorrect total bytes read. Expected " - + expectedBytesRead + " but actually " + bytesRead); - } - if (!keepGoing) { + if (consumed) { + long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize; + if (expectedBytesRead != bytesRead) { + throw new IllegalStateException("Incorrect total bytes read. Expected " + + expectedBytesRead + " but actually " + bytesRead); + } prepareForNextElement(); - return READ_RESULT_CONTINUE; } - break; + return READ_RESULT_CONTINUE; case TYPE_UNKNOWN: if (elementContentSize > Integer.MAX_VALUE) { throw new IllegalStateException( @@ -254,11 +244,11 @@ import java.util.Stack; if (skipResult != READ_RESULT_CONTINUE) { return skipResult; } + prepareForNextElement(); break; default: throw new IllegalStateException("Invalid element type " + type); } - prepareForNextElement(); } } @@ -508,7 +498,7 @@ import java.util.Stack; */ private int updateBytesState(int additionalBytesRead, int totalBytes) { if (additionalBytesRead == -1) { - return READ_RESULT_END_OF_FILE; + return READ_RESULT_END_OF_STREAM; } bytesState += additionalBytesRead; bytesRead += additionalBytesRead; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java index 351eff32d9..00f24bbad7 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.MimeTypes; import android.annotation.TargetApi; import android.media.MediaExtractor; +import java.nio.ByteBuffer; import java.util.Arrays; import java.util.concurrent.TimeUnit; @@ -77,13 +78,15 @@ public final class DefaultWebmExtractor implements WebmExtractor { private static final int LACING_FIXED = 2; private static final int LACING_EBML = 3; + private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM + | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; + private final EbmlReader reader; private final byte[] simpleBlockTimecodeAndFlags = new byte[3]; - private SampleHolder tempSampleHolder; - private boolean sampleRead; + private SampleHolder sampleHolder; + private int readResults; - private boolean prepared = false; private long segmentStartOffsetBytes = UNKNOWN; private long segmentEndOffsetBytes = UNKNOWN; private long timecodeScale = 1000000L; @@ -105,28 +108,29 @@ public final class DefaultWebmExtractor implements WebmExtractor { /* package */ DefaultWebmExtractor(EbmlReader reader) { this.reader = reader; this.reader.setEventHandler(new InnerEbmlEventHandler()); - this.cueTimesUs = new LongArray(); - this.cueClusterPositions = new LongArray(); } @Override - public boolean isPrepared() { - return prepared; - } - - @Override - public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { - tempSampleHolder = sampleHolder; - sampleRead = false; - reader.read(inputStream); - tempSampleHolder = null; - return sampleRead; + public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { + this.sampleHolder = sampleHolder; + this.readResults = 0; + while ((readResults & READ_TERMINATING_RESULTS) == 0) { + int ebmlReadResult = reader.read(inputStream); + if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) { + readResults |= WebmExtractor.RESULT_NEED_MORE_DATA; + } else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) { + readResults |= WebmExtractor.RESULT_END_OF_STREAM; + } + } + this.sampleHolder = null; + return readResults; } @Override public boolean seekTo(long seekTimeUs, boolean allowNoop) { - checkPrepared(); if (allowNoop + && cues != null + && clusterTimecodeUs != UNKNOWN && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) { int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs); @@ -134,19 +138,19 @@ public final class DefaultWebmExtractor implements WebmExtractor { return false; } } + clusterTimecodeUs = UNKNOWN; + simpleBlockTimecodeUs = UNKNOWN; reader.reset(); return true; } @Override - public SegmentIndex getCues() { - checkPrepared(); + public SegmentIndex getIndex() { return cues; } @Override public MediaFormat getFormat() { - checkPrepared(); return format; } @@ -196,6 +200,8 @@ public final class DefaultWebmExtractor implements WebmExtractor { break; case ID_CUES: cuesSizeBytes = headerSizeBytes + contentsSizeBytes; + cueTimesUs = new LongArray(); + cueClusterPositions = new LongArray(); break; default: // pass @@ -204,11 +210,16 @@ public final class DefaultWebmExtractor implements WebmExtractor { } /* package */ boolean onMasterElementEnd(int id) { - if (id == ID_CUES) { - finishPreparing(); - return false; + switch (id) { + case ID_CUES: + buildCues(); + return false; + case ID_VIDEO: + buildFormat(); + return true; + default: + return true; } - return true; } /* package */ boolean onIntegerElement(int id, long value) { @@ -283,6 +294,12 @@ public final class DefaultWebmExtractor implements WebmExtractor { // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure // for info about how data is organized in a SimpleBlock element. + // If we don't have a sample holder then don't consume the data. + if (sampleHolder == null) { + readResults |= RESULT_NEED_SAMPLE_HOLDER; + return false; + } + // Value of trackNumber is not used but needs to be read. reader.readVarint(inputStream); @@ -304,10 +321,10 @@ public final class DefaultWebmExtractor implements WebmExtractor { case LACING_NONE: long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; - tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; - tempSampleHolder.decodeOnly = invisible; - tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs; - tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); + sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + sampleHolder.decodeOnly = invisible; + sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; + sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); break; case LACING_EBML: case LACING_FIXED: @@ -316,44 +333,63 @@ public final class DefaultWebmExtractor implements WebmExtractor { throw new IllegalStateException("Lacing mode " + lacing + " not supported"); } - // Read video data into sample holder. - reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size); - sampleRead = true; - return false; - } else { - reader.skipBytes(inputStream, contentsSizeBytes); - return true; + ByteBuffer outputData = sampleHolder.data; + if (sampleHolder.allowDataBufferReplacement + && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) { + outputData = ByteBuffer.allocate(sampleHolder.size); + sampleHolder.data = outputData; + } + + if (outputData == null) { + reader.skipBytes(inputStream, sampleHolder.size); + sampleHolder.size = 0; + } else { + reader.readBytes(inputStream, outputData, sampleHolder.size); + } + readResults |= RESULT_READ_SAMPLE; } + return true; } private long scaleTimecodeToUs(long unscaledTimecode) { return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale); } - private void checkPrepared() { - if (!prepared) { - throw new IllegalStateException("Parser not yet prepared"); + /** + * Build a video {@link MediaFormat} containing recently gathered Video information, if needed. + * + *

Replaces the previous {@link #format} only if video width/height have changed. + * {@link #format} is guaranteed to not be null after calling this method. In + * the event that it can't be built, an {@link IllegalStateException} will be thrown. + */ + private void buildFormat() { + if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN + && (format == null || format.width != pixelWidth || format.height != pixelHeight)) { + format = MediaFormat.createVideoFormat( + MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); + readResults |= RESULT_READ_INIT; + } else if (format == null) { + throw new IllegalStateException("Unable to build format"); } } - private void finishPreparing() { - if (prepared) { - throw new IllegalStateException("Already prepared"); - } else if (segmentStartOffsetBytes == UNKNOWN) { + /** + * Build a {@link SegmentIndex} containing recently gathered Cues information. + * + *

{@link #cues} is guaranteed to not be null after calling this method. In + * the event that it can't be built, an {@link IllegalStateException} will be thrown. + */ + private void buildCues() { + if (segmentStartOffsetBytes == UNKNOWN) { throw new IllegalStateException("Segment start/end offsets unknown"); } else if (durationUs == UNKNOWN) { throw new IllegalStateException("Duration unknown"); - } else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) { - throw new IllegalStateException("Pixel width/height unknown"); } else if (cuesSizeBytes == UNKNOWN) { throw new IllegalStateException("Cues size unknown"); - } else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { + } else if (cueTimesUs == null || cueClusterPositions == null + || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { throw new IllegalStateException("Invalid/missing cue points"); } - - format = MediaFormat.createVideoFormat( - MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); - int cuePointsSize = cueTimesUs.size(); int[] sizes = new int[cuePointsSize]; long[] offsets = new long[cuePointsSize]; @@ -372,8 +408,7 @@ public final class DefaultWebmExtractor implements WebmExtractor { cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs); cueTimesUs = null; cueClusterPositions = null; - - prepared = true; + readResults |= RESULT_READ_INDEX; } /** @@ -388,30 +423,30 @@ public final class DefaultWebmExtractor implements WebmExtractor { } @Override - public boolean onMasterElementStart( + public void onMasterElementStart( int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { - return DefaultWebmExtractor.this.onMasterElementStart( + DefaultWebmExtractor.this.onMasterElementStart( id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes); } @Override - public boolean onMasterElementEnd(int id) { - return DefaultWebmExtractor.this.onMasterElementEnd(id); + public void onMasterElementEnd(int id) { + DefaultWebmExtractor.this.onMasterElementEnd(id); } @Override - public boolean onIntegerElement(int id, long value) { - return DefaultWebmExtractor.this.onIntegerElement(id, value); + public void onIntegerElement(int id, long value) { + DefaultWebmExtractor.this.onIntegerElement(id, value); } @Override - public boolean onFloatElement(int id, double value) { - return DefaultWebmExtractor.this.onFloatElement(id, value); + public void onFloatElement(int id, double value) { + DefaultWebmExtractor.this.onFloatElement(id, value); } @Override - public boolean onStringElement(int id, String value) { - return DefaultWebmExtractor.this.onStringElement(id, value); + public void onStringElement(int id, String value) { + DefaultWebmExtractor.this.onStringElement(id, value); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java index 42e26d4531..dcedf9a898 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java @@ -46,9 +46,8 @@ import java.nio.ByteBuffer; * @param elementOffsetBytes The byte offset where this element starts * @param headerSizeBytes The byte length of this element's ID and size header * @param contentsSizeBytes The byte length of this element's children - * @return {@code false} if parsing should stop right away */ - public boolean onMasterElementStart( + public void onMasterElementStart( int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes); /** @@ -56,44 +55,42 @@ import java.nio.ByteBuffer; * {@link NonBlockingInputStream}. * * @param id The integer ID of this element - * @return {@code false} if parsing should stop right away */ - public boolean onMasterElementEnd(int id); + public void onMasterElementEnd(int id); /** * Called when an integer element is encountered in the {@link NonBlockingInputStream}. * * @param id The integer ID of this element * @param value The integer value this element contains - * @return {@code false} if parsing should stop right away */ - public boolean onIntegerElement(int id, long value); + public void onIntegerElement(int id, long value); /** * Called when a float element is encountered in the {@link NonBlockingInputStream}. * * @param id The integer ID of this element * @param value The float value this element contains - * @return {@code false} if parsing should stop right away */ - public boolean onFloatElement(int id, double value); + public void onFloatElement(int id, double value); /** * Called when a string element is encountered in the {@link NonBlockingInputStream}. * * @param id The integer ID of this element * @param value The string value this element contains - * @return {@code false} if parsing should stop right away */ - public boolean onStringElement(int id, String value); + public void onStringElement(int id, String value); /** * Called when a binary element is encountered in the {@link NonBlockingInputStream}. * *

The element header (containing element ID and content size) will already have been read. - * Subclasses must exactly read the entire contents of the element, which is - * {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be - * immediately available from {@code inputStream}. + * Subclasses must either read nothing and return {@code false}, or exactly read the entire + * contents of the element, which is {@code contentsSizeBytes} in length, and return {@code true}. + * + *

It's guaranteed that the full element contents will be immediately available from + * {@code inputStream}. * *

Several methods in {@link EbmlReader} are available for reading the contents of a * binary element: @@ -111,7 +108,7 @@ import java.nio.ByteBuffer; * @param contentsSizeBytes The byte length of this element's contents * @param inputStream The {@link NonBlockingInputStream} from which this * element's contents should be read - * @return {@code false} if parsing should stop right away + * @return True if the element was read. False otherwise. */ public boolean onBinaryElement( int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java index a9bf11f699..dd1c43fce3 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java @@ -44,12 +44,12 @@ import java.nio.ByteBuffer; // Return values for reading methods. public static final int READ_RESULT_CONTINUE = 0; public static final int READ_RESULT_NEED_MORE_DATA = 1; - public static final int READ_RESULT_END_OF_FILE = 2; + public static final int READ_RESULT_END_OF_STREAM = 2; public void setEventHandler(EbmlEventHandler eventHandler); /** - * Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed. + * Reads from a {@link NonBlockingInputStream}, invoking an event callback if possible. * * @param inputStream The input stream from which data should be read * @return One of the {@code RESULT_*} flags defined in this interface diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java index 4ecefe7906..e824887476 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java @@ -30,24 +30,38 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; public interface WebmExtractor { /** - * Whether the has parsed the cues and sample format from the stream. - * - * @return True if the extractor is prepared. False otherwise + * An attempt to read from the input stream returned insufficient data. */ - public boolean isPrepared(); + public static final int RESULT_NEED_MORE_DATA = 1; + /** + * The end of the input stream was reached. + */ + public static final int RESULT_END_OF_STREAM = 2; + /** + * A media sample was read. + */ + public static final int RESULT_READ_SAMPLE = 4; + /** + * Initialization data was read. The parsed data can be read using {@link #getFormat()}. + */ + public static final int RESULT_READ_INIT = 8; + /** + * A sidx atom was read. The parsed data can be read using {@link #getIndex()}. + */ + public static final int RESULT_READ_INDEX = 16; + /** + * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. + */ + public static final int RESULT_NEED_SAMPLE_HOLDER = 32; /** * Consumes data from a {@link NonBlockingInputStream}. * - *

If the return value is {@code false}, then a sample may have been partially read into - * {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed - * in subsequent calls until the whole sample has been read. - * * @param inputStream The input stream from which data should be read * @param sampleHolder A {@link SampleHolder} into which the sample should be read - * @return {@code true} if a sample has been read into the sample holder + * @return One or more of the {@code RESULT_*} flags defined in this class. */ - public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder); + public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder); /** * Seeks to a position before or equal to the requested time. @@ -66,7 +80,7 @@ public interface WebmExtractor { * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet * prepared */ - public SegmentIndex getCues(); + public SegmentIndex getIndex(); /** * Returns the format of the samples contained within the media stream. diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 3e671d2302..1fd213eda1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -144,7 +144,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { @Override protected void doSomeWork(long timeUs) throws ExoPlaybackException { - source.continueBuffering(timeUs); + try { + source.continueBuffering(timeUs); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + currentPositionUs = timeUs; // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance @@ -225,7 +230,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { @Override protected long getBufferedPositionUs() { // Don't block playback whilst subtitles are loading. - return END_OF_TRACK; + return END_OF_TRACK_US; } @Override @@ -275,7 +280,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } } - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java b/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java index 2b7619595b..b3fb38921a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java @@ -24,6 +24,28 @@ package com.google.android.exoplayer.upstream; */ public interface Allocation { + /** + * Ensures the allocation has a capacity greater than or equal to the specified size in bytes. + *

+ * If {@code size} is greater than the current capacity of the allocation, then it will grow + * to have a capacity of at least {@code size}. The allocation is grown by adding new fragments. + * Existing fragments remain unchanged, and any data that has been written to them will be + * preserved. + *

+ * If {@code size} is less than or equal to the capacity of the allocation, then the call is a + * no-op. + * + * @param size The minimum required capacity, in bytes. + */ + public void ensureCapacity(int size); + + /** + * Gets the capacity of the allocation, in bytes. + * + * @return The capacity of the allocation, in bytes. + */ + public int capacity(); + /** * Gets the buffers in which the fragments are allocated. * diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java index 0007597e68..5bbffb6c1f 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java @@ -26,10 +26,10 @@ public interface BandwidthMeter { final long NO_ESTIMATE = -1; /** - * Gets the estimated bandwidth. + * Gets the estimated bandwidth, in bits/sec. * - * @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available. + * @return Estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if no estimate is available. */ - long getEstimate(); + long getBitrateEstimate(); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java index 979dc39e46..a7d847d5a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java @@ -67,15 +67,39 @@ public final class BufferPool implements Allocator { @Override public synchronized Allocation allocate(int size) { + return new AllocationImpl(allocate(size, null)); + } + + /** + * Allocates byte arrays whose combined length is at least {@code size}. + *

+ * An existing array of byte arrays may be provided to form the start of the allocation. + * + * @param size The total size required, in bytes. + * @param existing Existing byte arrays to use as the start of the allocation. May be null. + * @return The allocated byte arrays. + */ + /* package */ synchronized byte[][] allocate(int size, byte[][] existing) { int requiredBufferCount = requiredBufferCount(size); - allocatedBufferCount += requiredBufferCount; + if (existing != null && requiredBufferCount <= existing.length) { + // The existing buffers are sufficient. + return existing; + } + // We need to allocate additional buffers. byte[][] buffers = new byte[requiredBufferCount][]; - for (int i = 0; i < requiredBufferCount; i++) { + int firstNewBufferIndex = 0; + if (existing != null) { + firstNewBufferIndex = existing.length; + System.arraycopy(existing, 0, buffers, 0, firstNewBufferIndex); + } + // Allocate the new buffers + allocatedBufferCount += requiredBufferCount - firstNewBufferIndex; + for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) { // Use a recycled buffer if one is available. Else instantiate a new one. buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] : new byte[bufferLength]; } - return new AllocationImpl(buffers); + return buffers; } /** @@ -112,6 +136,16 @@ public final class BufferPool implements Allocator { this.buffers = buffers; } + @Override + public void ensureCapacity(int size) { + buffers = allocate(size, buffers); + } + + @Override + public int capacity() { + return bufferLength * buffers.length; + } + @Override public byte[][] getBuffers() { return buffers; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java index 18b79ea547..d672dd95d7 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import java.io.ByteArrayOutputStream; @@ -29,7 +30,7 @@ public class ByteArrayDataSink implements DataSink { @Override public DataSink open(DataSpec dataSpec) throws IOException { - if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) { + if (dataSpec.length == C.LENGTH_UNBOUNDED) { stream = new ByteArrayOutputStream(); } else { Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java index 937c9bc229..768d7061a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import java.io.IOException; @@ -36,14 +37,14 @@ public class ByteArrayDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { - if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) { + if (dataSpec.length == C.LENGTH_UNBOUNDED) { Assertions.checkArgument(dataSpec.position < data.length); } else { Assertions.checkArgument(dataSpec.position + dataSpec.length <= data.length); } readPosition = (int) dataSpec.position; - return (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) - ? (data.length - dataSpec.position) : dataSpec.length; + return (dataSpec.length == C.LENGTH_UNBOUNDED) ? (data.length - dataSpec.position) + : dataSpec.length; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java index 596bf764c3..624e42a111 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; + import java.io.IOException; /** @@ -34,9 +36,10 @@ public interface DataSource { * @param dataSpec Defines the data to be read. * @throws IOException If an error occurs opening the source. * @return The number of bytes that can be read from the opened source. For unbounded requests - * (i.e. requests where {@link DataSpec#length} equals {@link DataSpec#LENGTH_UNBOUNDED}) - * this value is the resolved length of the request. For all other requests, the value - * returned will be equal to the request's {@link DataSpec#length}. + * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNBOUNDED}) this value + * is the resolved length of the request, or {@link C#LENGTH_UNBOUNDED} if the length is still + * unresolved. For all other requests, the value returned will be equal to the request's + * {@link DataSpec#length}. */ public long open(DataSpec dataSpec) throws IOException; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java index 9060c88567..dc5227e426 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; @@ -39,6 +40,8 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream } + private static final int CHUNKED_ALLOCATION_INCREMENT = 256 * 1024; + private final DataSource dataSource; private final DataSpec dataSpec; private final Allocator allocator; @@ -57,7 +60,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream /** * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed - * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * the length resolved by {@code dataSource.open(dataSpec)} must not exceed * {@link Integer#MAX_VALUE}. * @param allocator Used to obtain an {@link Allocation} for holding the data. @@ -67,7 +70,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream this.dataSource = dataSource; this.dataSpec = dataSpec; this.allocator = allocator; - resolvedLength = DataSpec.LENGTH_UNBOUNDED; + resolvedLength = C.LENGTH_UNBOUNDED; readHead = new ReadHead(); } @@ -97,13 +100,14 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream } /** - * Returns the length of the streamin bytes. + * Returns the length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has + * yet to be determined. * - * @return The length of the stream in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length - * has yet to be determined. + * @return The length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has + * yet to be determined. */ public long getLength() { - return resolvedLength != DataSpec.LENGTH_UNBOUNDED ? resolvedLength : dataSpec.length; + return resolvedLength != C.LENGTH_UNBOUNDED ? resolvedLength : dataSpec.length; } /** @@ -112,7 +116,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream * @return True if the stream has finished loading. False otherwise. */ public boolean isLoadFinished() { - return resolvedLength != DataSpec.LENGTH_UNBOUNDED && loadPosition == resolvedLength; + return resolvedLength != C.LENGTH_UNBOUNDED && loadPosition == resolvedLength; } /** @@ -123,7 +127,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream * Note: The read methods provide a more efficient way of consuming the loaded data. Use this * method only when a freshly allocated byte[] containing all of the loaded data is required. * - * @return The loaded data or null. + * @return The loaded data, or null. */ public final byte[] getLoadedData() { if (loadPosition == 0) { @@ -144,7 +148,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream @Override public boolean isEndOfStream() { - return resolvedLength != DataSpec.LENGTH_UNBOUNDED && readHead.position == resolvedLength; + return resolvedLength != C.LENGTH_UNBOUNDED && readHead.position == resolvedLength; } @Override @@ -191,6 +195,11 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream int bytesRead = 0; byte[][] buffers = allocation.getBuffers(); while (bytesRead < bytesToRead) { + if (readHead.fragmentRemaining == 0) { + readHead.fragmentIndex++; + readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex); + readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex); + } int bufferReadLength = Math.min(readHead.fragmentRemaining, bytesToRead - bytesRead); if (target != null) { target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength); @@ -203,11 +212,6 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream bytesRead += bufferReadLength; readHead.fragmentOffset += bufferReadLength; readHead.fragmentRemaining -= bufferReadLength; - if (readHead.fragmentRemaining == 0 && readHead.position < resolvedLength) { - readHead.fragmentIndex++; - readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex); - readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex); - } } return bytesRead; @@ -231,23 +235,32 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream // The load was canceled, or is already complete. return; } + try { DataSpec loadDataSpec; - if (resolvedLength == DataSpec.LENGTH_UNBOUNDED) { + if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) { loadDataSpec = dataSpec; - resolvedLength = dataSource.open(loadDataSpec); + long resolvedLength = dataSource.open(loadDataSpec); if (resolvedLength > Integer.MAX_VALUE) { throw new DataSourceStreamLoadException( new UnexpectedLengthException(dataSpec.length, resolvedLength)); } + this.resolvedLength = resolvedLength; } else { + long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED + ? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED; loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, - resolvedLength - loadPosition, dataSpec.key); + remainingLength, dataSpec.key); dataSource.open(loadDataSpec); } + if (allocation == null) { - allocation = allocator.allocate((int) resolvedLength); + int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED + ? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT; + allocation = allocator.allocate(initialAllocationSize); } + int allocationCapacity = allocation.capacity(); + if (loadPosition == 0) { writeFragmentIndex = 0; writeFragmentOffset = allocation.getFragmentOffset(0); @@ -256,22 +269,28 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream int read = Integer.MAX_VALUE; byte[][] buffers = allocation.getBuffers(); - while (!loadCanceled && loadPosition < resolvedLength && read > 0) { + while (!loadCanceled && read > 0 && maybeMoreToLoad()) { if (Thread.interrupted()) { throw new InterruptedException(); } - int writeLength = (int) Math.min(writeFragmentRemainingLength, - resolvedLength - loadPosition); - read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, writeLength); + read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, + writeFragmentRemainingLength); if (read > 0) { loadPosition += read; writeFragmentOffset += read; writeFragmentRemainingLength -= read; - if (writeFragmentRemainingLength == 0 && loadPosition < resolvedLength) { + if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) { writeFragmentIndex++; + if (loadPosition == allocationCapacity) { + allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT); + allocationCapacity = allocation.capacity(); + buffers = allocation.getBuffers(); + } writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex); writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex); } + } else if (resolvedLength == C.LENGTH_UNBOUNDED) { + resolvedLength = loadPosition; } else if (resolvedLength != loadPosition) { throw new DataSourceStreamLoadException( new UnexpectedLengthException(resolvedLength, loadPosition)); @@ -282,6 +301,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream } } + private boolean maybeMoreToLoad() { + return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength; + } + private static class ReadHead { private int position; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java index d9f5030d19..41f758be8a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import android.net.Uri; @@ -24,13 +25,6 @@ import android.net.Uri; */ public final class DataSpec { - /** - * A permitted value of {@link #length}. A {@link DataSpec} defined with this length represents - * the region of media data that starts at its {@link #position} and extends to the end of the - * data whose location is defined by its {@link #uri}. - */ - public static final int LENGTH_UNBOUNDED = -1; - /** * Identifies the source from which data should be read. */ @@ -50,7 +44,7 @@ public final class DataSpec { */ public final long position; /** - * The length of the data. Greater than zero, or equal to {@link #LENGTH_UNBOUNDED}. + * The length of the data. Greater than zero, or equal to {@link C#LENGTH_UNBOUNDED}. */ public final long length; /** @@ -98,7 +92,7 @@ public final class DataSpec { boolean uriIsFullStream) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); - Assertions.checkArgument(length > 0 || length == LENGTH_UNBOUNDED); + Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED); Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream); this.uri = uri; this.uriIsFullStream = uriIsFullStream; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java index 4b2ed2806c..ce2197e2cf 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java @@ -38,11 +38,11 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { * * @param elapsedMs The time taken to transfer the bytes, in milliseconds. * @param bytes The number of bytes transferred. - * @param bandwidthEstimate The estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no - * estimate is available. Note that this estimate is typically derived from more information - * than {@code bytes} and {@code elapsedMs}. + * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate + * is available. Note that this estimate is typically derived from more information than + * {@code bytes} and {@code elapsedMs}. */ - void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate); + void onBandwidthSample(int elapsedMs, long bytes, long bitrate); } @@ -53,9 +53,9 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { private final Clock clock; private final SlidingPercentile slidingPercentile; - private long accumulator; + private long bytesAccumulator; private long startTimeMs; - private long bandwidthEstimate; + private long bitrateEstimate; private int streamCount; public DefaultBandwidthMeter() { @@ -80,17 +80,12 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { this.eventListener = eventListener; this.clock = clock; this.slidingPercentile = new SlidingPercentile(maxWeight); - bandwidthEstimate = NO_ESTIMATE; + bitrateEstimate = NO_ESTIMATE; } - /** - * Gets the estimated bandwidth. - * - * @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available. - */ @Override - public synchronized long getEstimate() { - return bandwidthEstimate; + public synchronized long getBitrateEstimate() { + return bitrateEstimate; } @Override @@ -103,7 +98,7 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { @Override public synchronized void onBytesTransferred(int bytes) { - accumulator += bytes; + bytesAccumulator += bytes; } @Override @@ -112,32 +107,26 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { long nowMs = clock.elapsedRealtime(); int elapsedMs = (int) (nowMs - startTimeMs); if (elapsedMs > 0) { - float bytesPerSecond = accumulator * 1000 / elapsedMs; - slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond); + float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs; + slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond); float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); - bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE + bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE : (long) bandwidthEstimateFloat; - notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate); + notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate); } streamCount--; if (streamCount > 0) { startTimeMs = nowMs; } - accumulator = 0; + bytesAccumulator = 0; } - // TODO: Use media time (bytes / mediaRate) as weight. - private int computeWeight(long mediaBytes) { - return (int) Math.sqrt(mediaBytes); - } - - private void notifyBandwidthSample(final int elapsedMs, final long bytes, - final long bandwidthEstimate) { + private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate); + eventListener.onBandwidthSample(elapsedMs, bytes, bitrate); } }); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java index 0ca8d3ea80..a08cacb982 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; + import java.io.IOException; import java.io.RandomAccessFile; @@ -42,8 +44,7 @@ public final class FileDataSource implements DataSource { try { file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); file.seek(dataSpec.position); - bytesRemaining = dataSpec.length == DataSpec.LENGTH_UNBOUNDED - ? file.length() - dataSpec.position + bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position : dataSpec.length; return bytesRemaining; } catch (IOException e) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java index 374a648efa..f9d3bf8f1a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Predicate; import com.google.android.exoplayer.util.Util; @@ -258,16 +259,9 @@ public class HttpDataSource implements DataSource { } long contentLength = getContentLength(connection); - dataLength = dataSpec.length == DataSpec.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; - if (dataLength == DataSpec.LENGTH_UNBOUNDED) { - // The DataSpec specified unbounded length and we failed to resolve a length from the - // response headers. - throw new HttpDataSourceException( - new UnexpectedLengthException(DataSpec.LENGTH_UNBOUNDED, DataSpec.LENGTH_UNBOUNDED), - dataSpec); - } + dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; - if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED && contentLength != DataSpec.LENGTH_UNBOUNDED + if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED && contentLength != dataSpec.length) { // The DataSpec specified a length and we resolved a length from the response headers, but // the two lengths do not match. @@ -305,9 +299,9 @@ public class HttpDataSource implements DataSource { if (listener != null) { listener.onBytesTransferred(read); } - } else if (dataLength != bytesRead) { + } else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) { // Check for cases where the server closed the connection having not sent the correct amount - // of data. + // of data. We can only do this if we know the length of the data we were expecting. throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead), dataSpec); } @@ -364,14 +358,15 @@ public class HttpDataSource implements DataSource { } /** - * Returns the number of bytes that are still to be read for the current {@link DataSpec}. This - * value is equivalent to {@code dataSpec.length - bytesRead()}, where dataSpec is the - * {@link DataSpec} that was passed to the most recent call of {@link #open(DataSpec)}. + * Returns the number of bytes that are still to be read for the current {@link DataSpec}. + *

+ * If the total length of the data being read is known, then this length minus {@code bytesRead()} + * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned. * - * @return The number of bytes remaining. + * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}. */ protected final long bytesRemaining() { - return dataLength - bytesRead; + return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead; } private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { @@ -394,14 +389,14 @@ public class HttpDataSource implements DataSource { private String buildRangeHeader(DataSpec dataSpec) { String rangeRequest = "bytes=" + dataSpec.position + "-"; - if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED) { + if (dataSpec.length != C.LENGTH_UNBOUNDED) { rangeRequest += (dataSpec.position + dataSpec.length - 1); } return rangeRequest; } private long getContentLength(HttpURLConnection connection) { - long contentLength = DataSpec.LENGTH_UNBOUNDED; + long contentLength = C.LENGTH_UNBOUNDED; String contentLengthHeader = connection.getHeaderField("Content-Length"); if (!TextUtils.isEmpty(contentLengthHeader)) { try { @@ -435,10 +430,6 @@ public class HttpDataSource implements DataSource { } } } - if (contentLength == DataSpec.LENGTH_UNBOUNDED) { - Log.w(TAG, "Unable to parse content length [" + contentLengthHeader + "] [" + - contentRangeHeader + "]"); - } return contentLength; } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java index b54db19843..cbb571f308 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import java.io.IOException; @@ -39,7 +40,7 @@ public final class TeeDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { long dataLength = upstream.open(dataSpec); - if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) { + if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) { // Reconstruct dataSpec in order to provide the resolved length to the sink. dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength, dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java index 02177f7d93..942a29f0c7 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream.cache; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSink; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.util.Assertions; @@ -63,6 +64,9 @@ public class CacheDataSink implements DataSink { @Override public DataSink open(DataSpec dataSpec) throws CacheDataSinkException { + // TODO: Support caching for unbounded requests. See TODO in {@link CacheDataSource} for + // more details. + Assertions.checkState(dataSpec.length != C.LENGTH_UNBOUNDED); try { this.dataSpec = dataSpec; dataSpecBytesWritten = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java index 1a0ebf3e19..379b63fa53 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.upstream.cache; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSink; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; @@ -34,10 +35,26 @@ import java.io.IOException; */ public final class CacheDataSource implements DataSource { + /** + * Interface definition for a callback to be notified of {@link CacheDataSource} events. + */ + public interface EventListener { + + /** + * Invoked when bytes have been read from {@link #cache} since the last invocation. + * + * @param cacheSizeBytes Current cache size in bytes. + * @param cachedBytesRead Total bytes read from {@link #cache} since last report. + */ + void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); + + } + private final Cache cache; private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; + private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; @@ -49,6 +66,7 @@ public final class CacheDataSource implements DataSource { private long bytesRemaining; private CacheSpan lockedSpan; private boolean ignoreCache; + private long totalCachedBytesRead; /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for @@ -67,7 +85,7 @@ public final class CacheDataSource implements DataSource { public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache, boolean ignoreCacheOnError, long maxCacheFileSize) { this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize), - blockOnCache, ignoreCacheOnError); + blockOnCache, ignoreCacheOnError, null); } /** @@ -84,9 +102,11 @@ public final class CacheDataSource implements DataSource { * @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If * true, then cache related exceptions may be thrown for one cycle of open, read and close * calls. Subsequent cycles of these calls will then bypass the cache. + * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, - DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError) { + DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError, + EventListener eventListener) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = blockOnCache; @@ -97,6 +117,7 @@ public final class CacheDataSource implements DataSource { } else { this.cacheWriteDataSource = null; } + this.eventListener = eventListener; } @Override @@ -104,7 +125,7 @@ public final class CacheDataSource implements DataSource { Assertions.checkState(dataSpec.uriIsFullStream); // TODO: Support caching for unbounded requests. This requires storing the source length // into the cache (the simplest approach is to incorporate it into each cache file's name). - Assertions.checkState(dataSpec.length != DataSpec.LENGTH_UNBOUNDED); + Assertions.checkState(dataSpec.length != C.LENGTH_UNBOUNDED); try { uri = dataSpec.uri; key = dataSpec.key; @@ -121,10 +142,13 @@ public final class CacheDataSource implements DataSource { @Override public int read(byte[] buffer, int offset, int max) throws IOException { try { - int num = currentDataSource.read(buffer, offset, max); - if (num >= 0) { - readPosition += num; - bytesRemaining -= num; + int bytesRead = currentDataSource.read(buffer, offset, max); + if (bytesRead >= 0) { + if (currentDataSource == cacheReadDataSource) { + totalCachedBytesRead += bytesRead; + } + readPosition += bytesRead; + bytesRemaining -= bytesRead; } else { closeCurrentSource(); if (bytesRemaining > 0) { @@ -132,7 +156,7 @@ public final class CacheDataSource implements DataSource { return read(buffer, offset, max); } } - return num; + return bytesRead; } catch (IOException e) { handleBeforeThrow(e); throw e; @@ -141,6 +165,7 @@ public final class CacheDataSource implements DataSource { @Override public void close() throws IOException { + notifyBytesRead(); try { closeCurrentSource(); } catch (IOException e) { @@ -215,4 +240,11 @@ public final class CacheDataSource implements DataSource { } } + private void notifyBytesRead() { + if (eventListener != null && totalCachedBytesRead > 0) { + eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead); + totalCachedBytesRead = 0; + } + } + }