Commit 79c2f535 by ojw28

Merge pull request #29 from google/dev

Merge 1.0.12 to master
parents 553a1d2e cc04fd1e
Showing with 921 additions and 503 deletions
# 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
...@@ -55,6 +55,22 @@ accompanying demo application. To get started: ...@@ -55,6 +55,22 @@ accompanying demo application. To get started:
## Using Gradle ## ## 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.
...@@ -19,7 +19,7 @@ buildscript { ...@@ -19,7 +19,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.10.+' classpath 'com.android.tools.build:gradle:0.12.+'
} }
} }
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.demo" package="com.google.android.exoplayer.demo"
android:versionCode="1010" android:versionCode="1012"
android:versionName="1.0.10" android:versionName="1.0.12"
android:theme="@style/RootTheme"> android:theme="@style/RootTheme">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
......
...@@ -80,9 +80,9 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -80,9 +80,9 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
// DemoPlayer.InfoListener // DemoPlayer.InfoListener
@Override @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 + Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes +
", " + getTimeString(elapsedMs) + ", " + bandwidthEstimate + "]"); ", " + getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
} }
@Override @Override
...@@ -92,7 +92,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -92,7 +92,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
@Override @Override
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, 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(); loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) { if (VerboseLogUtil.isTagEnabled(TAG)) {
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId
...@@ -101,7 +101,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -101,7 +101,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onLoadCompleted(int sourceId) { public void onLoadCompleted(int sourceId, long bytesLoaded) {
if (VerboseLogUtil.isTagEnabled(TAG)) { if (VerboseLogUtil.isTagEnabled(TAG)) {
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " +
......
...@@ -98,12 +98,12 @@ import android.widget.TextView; ...@@ -98,12 +98,12 @@ import android.widget.TextView;
@Override @Override
protected long getDurationUs() { protected long getDurationUs() {
return TrackRenderer.MATCH_LONGEST; return TrackRenderer.MATCH_LONGEST_US;
} }
@Override @Override
protected long getBufferedPositionUs() { protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK; return TrackRenderer.END_OF_TRACK_US;
} }
@Override @Override
......
...@@ -121,10 +121,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -121,10 +121,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onDroppedFrames(int count, long elapsed); 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, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); int mediaStartTimeMs, int mediaEndTimeMs, long length);
void onLoadCompleted(int sourceId); void onLoadCompleted(int sourceId, long bytesLoaded);
} }
/** /**
...@@ -391,9 +391,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -391,9 +391,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) { public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
if (infoListener != null) { 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 ...@@ -471,34 +471,34 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override @Override
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, 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) { if (infoListener != null) {
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
mediaEndTimeMs, totalBytes); mediaEndTimeMs, length);
} }
} }
@Override @Override
public void onLoadCompleted(int sourceId) { public void onLoadCompleted(int sourceId, long bytesLoaded) {
if (infoListener != null) { if (infoListener != null) {
infoListener.onLoadCompleted(sourceId); infoListener.onLoadCompleted(sourceId, bytesLoaded);
} }
} }
@Override @Override
public void onLoadCanceled(int sourceId) { public void onLoadCanceled(int sourceId, long bytesLoaded) {
// Do nothing. // Do nothing.
} }
@Override @Override
public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long totalBytes) { long bytesDiscarded) {
// Do nothing. // Do nothing.
} }
@Override @Override
public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long totalBytes) { long bytesDiscarded) {
// Do nothing. // Do nothing.
} }
......
...@@ -36,3 +36,14 @@ android { ...@@ -36,3 +36,14 @@ android {
dependencies { 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);
}
/*
* 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() {}
}
...@@ -17,54 +17,41 @@ package com.google.android.exoplayer; ...@@ -17,54 +17,41 @@ package com.google.android.exoplayer;
/** /**
* Maintains codec event counts, for debugging purposes only. * Maintains codec event counts, for debugging purposes only.
* <p>
* 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 final class CodecCounters {
public volatile long codecInitCount; public int codecInitCount;
public volatile long codecReleaseCount; public int codecReleaseCount;
public volatile long outputFormatChangedCount; public int outputFormatChangedCount;
public volatile long outputBuffersChangedCount; public int outputBuffersChangedCount;
public volatile long queuedInputBufferCount; public int renderedOutputBufferCount;
public volatile long inputBufferWaitingForSampleCount; public int skippedOutputBufferCount;
public volatile long keyframeCount; public int droppedOutputBufferCount;
public volatile long queuedEndOfStreamCount;
public volatile long renderedOutputBufferCount;
public volatile long skippedOutputBufferCount;
public volatile long droppedOutputBufferCount;
public volatile long discardedSamplesCount;
/** /**
* 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() { public synchronized void ensureUpdated() {
codecInitCount = 0; // Do nothing. The use of synchronized ensures a memory barrier should another thread also
codecReleaseCount = 0; // call this method.
outputFormatChangedCount = 0;
outputBuffersChangedCount = 0;
queuedInputBufferCount = 0;
inputBufferWaitingForSampleCount = 0;
keyframeCount = 0;
queuedEndOfStreamCount = 0;
renderedOutputBufferCount = 0;
skippedOutputBufferCount = 0;
droppedOutputBufferCount = 0;
discardedSamplesCount = 0;
} }
public String getDebugString() { public String getDebugString() {
ensureUpdated();
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("cic(").append(codecInitCount).append(")"); builder.append("cic(").append(codecInitCount).append(")");
builder.append("crc(").append(codecReleaseCount).append(")"); builder.append("crc(").append(codecReleaseCount).append(")");
builder.append("ofc(").append(outputFormatChangedCount).append(")"); builder.append("ofc(").append(outputFormatChangedCount).append(")");
builder.append("obc(").append(outputBuffersChangedCount).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("ren(").append(renderedOutputBufferCount).append(")");
builder.append("sob(").append(skippedOutputBufferCount).append(")"); builder.append("sob(").append(skippedOutputBufferCount).append(")");
builder.append("dob(").append(droppedOutputBufferCount).append(")"); builder.append("dob(").append(droppedOutputBufferCount).append(")");
builder.append("dsc(").append(discardedSamplesCount).append(")");
return builder.toString(); return builder.toString();
} }
......
...@@ -316,14 +316,16 @@ public interface ExoPlayer { ...@@ -316,14 +316,16 @@ public interface ExoPlayer {
public void seekTo(int positionMs); public void seekTo(int positionMs);
/** /**
* Stops playback. * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
* is to pause playback.
* <p> * <p>
* Calling this method will cause the playback state to transition to * 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#STATE_IDLE}. The player instance can still be used, and
* {@link ExoPlayer#release()} must still be called on the player should it no longer be required. * {@link ExoPlayer#release()} must still be called on the player if it's no longer required.
* <p> * <p>
* Use {@code setPlayWhenReady(false)} rather than this method if the intention is to pause * Calling this method does not reset the playback position. If this player instance will be used
* playback. * 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(); public void stop();
......
...@@ -60,7 +60,7 @@ import java.util.List; ...@@ -60,7 +60,7 @@ import java.util.List;
private static final int IDLE_INTERVAL_MS = 1000; private static final int IDLE_INTERVAL_MS = 1000;
private final Handler handler; private final Handler handler;
private final HandlerThread internalPlayerThread; private final HandlerThread internalPlaybackThread;
private final Handler eventHandler; private final Handler eventHandler;
private final MediaClock mediaClock; private final MediaClock mediaClock;
private final boolean[] rendererEnabledFlags; private final boolean[] rendererEnabledFlags;
...@@ -95,12 +95,12 @@ import java.util.List; ...@@ -95,12 +95,12 @@ import java.util.List;
} }
this.state = ExoPlayer.STATE_IDLE; this.state = ExoPlayer.STATE_IDLE;
this.durationUs = TrackRenderer.UNKNOWN_TIME; this.durationUs = TrackRenderer.UNKNOWN_TIME_US;
this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME; this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
mediaClock = new MediaClock(); mediaClock = new MediaClock();
enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length); enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
internalPlayerThread = new HandlerThread(getClass().getSimpleName() + ":Handler") { internalPlaybackThread = new HandlerThread(getClass().getSimpleName() + ":Handler") {
@Override @Override
public void run() { public void run() {
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
...@@ -109,12 +109,12 @@ import java.util.List; ...@@ -109,12 +109,12 @@ import java.util.List;
super.run(); super.run();
} }
}; };
internalPlayerThread.start(); internalPlaybackThread.start();
handler = new Handler(internalPlayerThread.getLooper(), this); handler = new Handler(internalPlaybackThread.getLooper(), this);
} }
public Looper getPlaybackLooper() { public Looper getPlaybackLooper() {
return internalPlayerThread.getLooper(); return internalPlaybackThread.getLooper();
} }
public int getCurrentPosition() { public int getCurrentPosition() {
...@@ -122,12 +122,12 @@ import java.util.List; ...@@ -122,12 +122,12 @@ import java.util.List;
} }
public int getBufferedPosition() { public int getBufferedPosition() {
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: (int) (bufferedPositionUs / 1000); : (int) (bufferedPositionUs / 1000);
} }
public int getDuration() { public int getDuration() {
return durationUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: (int) (durationUs / 1000); : (int) (durationUs / 1000);
} }
...@@ -179,7 +179,7 @@ import java.util.List; ...@@ -179,7 +179,7 @@ import java.util.List;
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
internalPlayerThread.quit(); internalPlaybackThread.quit();
} }
} }
...@@ -287,14 +287,14 @@ import java.util.List; ...@@ -287,14 +287,14 @@ import java.util.List;
enabledRenderers.add(renderer); enabledRenderers.add(renderer);
isEnded = isEnded && renderer.isEnded(); isEnded = isEnded && renderer.isEnded();
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); 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 // 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. // duration is unknown regardless of the duration of this track.
} else { } else {
long trackDurationUs = renderer.getDurationUs(); long trackDurationUs = renderer.getDurationUs();
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME) { if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) {
durationUs = TrackRenderer.UNKNOWN_TIME; durationUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST) { } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) {
// Do nothing. // Do nothing.
} else { } else {
durationUs = Math.max(durationUs, trackDurationUs); durationUs = Math.max(durationUs, trackDurationUs);
...@@ -331,11 +331,11 @@ import java.util.List; ...@@ -331,11 +331,11 @@ import java.util.List;
long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs; long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
return minBufferDurationUs <= 0 return minBufferDurationUs <= 0
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs || rendererBufferedPositionUs >= positionUs + minBufferDurationUs
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs); && rendererBufferedPositionUs >= rendererDurationUs);
} }
...@@ -384,7 +384,7 @@ import java.util.List; ...@@ -384,7 +384,7 @@ import java.util.List;
private void doSomeWork() throws ExoPlaybackException { private void doSomeWork() throws ExoPlaybackException {
TraceUtil.beginSection("doSomeWork"); TraceUtil.beginSection("doSomeWork");
long operationStartTimeMs = SystemClock.elapsedRealtime(); long operationStartTimeMs = SystemClock.elapsedRealtime();
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME ? durationUs long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs
: Long.MAX_VALUE; : Long.MAX_VALUE;
boolean isEnded = true; boolean isEnded = true;
boolean allRenderersReadyOrEnded = true; boolean allRenderersReadyOrEnded = true;
...@@ -398,17 +398,17 @@ import java.util.List; ...@@ -398,17 +398,17 @@ import java.util.List;
isEnded = isEnded && renderer.isEnded(); isEnded = isEnded && renderer.isEnded();
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); 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 // 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. // media buffer position unknown regardless of the buffered position of this track.
} else { } else {
long rendererDurationUs = renderer.getDurationUs(); long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME) { if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME; bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs)) { && rendererBufferedPositionUs >= rendererDurationUs)) {
// This track is fully buffered. // This track is fully buffered.
} else { } else {
...@@ -525,7 +525,7 @@ import java.util.List; ...@@ -525,7 +525,7 @@ import java.util.List;
notifyAll(); 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. // The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK); handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} }
......
...@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { ...@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * 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. * The version of the library, expressed as an integer.
...@@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { ...@@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo {
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 1002003. * 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} * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
......
...@@ -67,12 +67,12 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -67,12 +67,12 @@ public final class FrameworkSampleSource implements SampleSource {
extractor = new MediaExtractor(); extractor = new MediaExtractor();
extractor.setDataSource(context, uri, headers); extractor.setDataSource(context, uri, headers);
trackStates = new int[extractor.getTrackCount()]; trackStates = new int[extractor.getTrackCount()];
pendingDiscontinuities = new boolean[extractor.getTrackCount()]; pendingDiscontinuities = new boolean[trackStates.length];
trackInfos = new TrackInfo[trackStates.length]; trackInfos = new TrackInfo[trackStates.length];
for (int i = 0; i < trackStates.length; i++) { for (int i = 0; i < trackStates.length; i++) {
android.media.MediaFormat format = extractor.getTrackFormat(i); android.media.MediaFormat format = extractor.getTrackFormat(i);
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? 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); String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, duration); trackInfos[i] = new TrackInfo(mime, duration);
} }
...@@ -84,7 +84,7 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -84,7 +84,7 @@ public final class FrameworkSampleSource implements SampleSource {
@Override @Override
public int getTrackCount() { public int getTrackCount() {
Assertions.checkState(prepared); Assertions.checkState(prepared);
return extractor.getTrackCount(); return trackStates.length;
} }
@Override @Override
...@@ -97,17 +97,18 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -97,17 +97,18 @@ public final class FrameworkSampleSource implements SampleSource {
public void enable(int track, long timeUs) { public void enable(int track, long timeUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
boolean wasSourceEnabled = isEnabled();
trackStates[track] = TRACK_STATE_ENABLED; trackStates[track] = TRACK_STATE_ENABLED;
extractor.selectTrack(track); extractor.selectTrack(track);
if (!wasSourceEnabled) { seekToUs(timeUs);
seekToUs(timeUs);
}
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { public boolean continueBuffering(long playbackPositionUs) {
// Do nothing. The MediaExtractor instance is responsible for buffering. // 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 @Override
...@@ -122,15 +123,15 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -122,15 +123,15 @@ public final class FrameworkSampleSource implements SampleSource {
if (onlyReadDiscontinuity) { if (onlyReadDiscontinuity) {
return NOTHING_READ; 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(); int extractorTrackIndex = extractor.getSampleTrackIndex();
if (extractorTrackIndex == track) { 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) { if (sampleHolder.data != null) {
int offset = sampleHolder.data.position(); int offset = sampleHolder.data.position();
sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset); sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
...@@ -187,7 +188,7 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -187,7 +188,7 @@ public final class FrameworkSampleSource implements SampleSource {
Assertions.checkState(prepared); Assertions.checkState(prepared);
long bufferedDurationUs = extractor.getCachedDuration(); long bufferedDurationUs = extractor.getCachedDuration();
if (bufferedDurationUs == -1) { if (bufferedDurationUs == -1) {
return TrackRenderer.UNKNOWN_TIME; return TrackRenderer.UNKNOWN_TIME_US;
} else { } else {
return extractor.getSampleTime() + bufferedDurationUs; return extractor.getSampleTime() + bufferedDurationUs;
} }
...@@ -202,13 +203,4 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -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;
}
} }
...@@ -266,8 +266,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -266,8 +266,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected void onOutputFormatChanged(MediaFormat format) { protected void onOutputFormatChanged(MediaFormat format) {
releaseAudioTrack();
this.sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int channelConfig; int channelConfig;
switch (channelCount) { switch (channelCount) {
...@@ -283,6 +281,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -283,6 +281,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
default: default:
throw new IllegalArgumentException("Unsupported channel count: " + channelCount); 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.channelConfig = channelConfig;
this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT); AudioFormat.ENCODING_PCM_16BIT);
...@@ -417,7 +425,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -417,7 +425,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean isReady() { protected boolean isReady() {
return getPendingFrameCount() > 0; return super.isReady() || getPendingFrameCount() > 0;
} }
/** /**
......
...@@ -128,6 +128,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -128,6 +128,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private int codecReconfigurationState; private int codecReconfigurationState;
private int trackIndex; private int trackIndex;
private boolean sourceIsReady;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private boolean outputStreamEnded; private boolean outputStreamEnded;
private boolean waitingForKeys; private boolean waitingForKeys;
...@@ -186,7 +187,12 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -186,7 +187,12 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
return TrackRenderer.STATE_IGNORE; 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) { protected boolean handlesMimeType(String mimeType) {
return true; return true;
// TODO: Uncomment once the TODO above is fixed. // TODO: Uncomment once the TODO above is fixed.
...@@ -196,6 +202,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -196,6 +202,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected void onEnabled(long timeUs, boolean joining) { protected void onEnabled(long timeUs, boolean joining) {
source.enable(trackIndex, timeUs); source.enable(trackIndex, timeUs);
sourceIsReady = false;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
waitingForKeys = false; waitingForKeys = false;
...@@ -280,14 +287,20 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -280,14 +287,20 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
releaseCodec();
format = null; format = null;
drmInitData = null; drmInitData = null;
if (openedDrmSession) { try {
drmSessionManager.close(); releaseCodec();
openedDrmSession = false; } finally {
try {
if (openedDrmSession) {
drmSessionManager.close();
openedDrmSession = false;
}
} finally {
source.disable(trackIndex);
}
} }
source.disable(trackIndex);
} }
protected void releaseCodec() { protected void releaseCodec() {
...@@ -332,7 +345,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -332,7 +345,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected long getBufferedPositionUs() { protected long getBufferedPositionUs() {
long sourceBufferedPosition = source.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()); ? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
} }
...@@ -340,6 +353,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -340,6 +353,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected void seekTo(long timeUs) throws ExoPlaybackException { protected void seekTo(long timeUs) throws ExoPlaybackException {
currentPositionUs = timeUs; currentPositionUs = timeUs;
source.seekToUs(timeUs); source.seekToUs(timeUs);
sourceIsReady = false;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
waitingForKeys = false; waitingForKeys = false;
...@@ -358,7 +372,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -358,7 +372,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException { protected void doSomeWork(long timeUs) throws ExoPlaybackException {
try { try {
source.continueBuffering(timeUs); sourceIsReady = source.continueBuffering(timeUs);
checkForDiscontinuity(); checkForDiscontinuity();
if (format == null) { if (format == null) {
readFormat(); readFormat();
...@@ -373,6 +387,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -373,6 +387,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
while (feedInputBuffer()) {} while (feedInputBuffer()) {}
} }
} }
codecCounters.ensureUpdated();
} catch (IOException e) { } catch (IOException e) {
throw new ExoPlaybackException(e); throw new ExoPlaybackException(e);
} }
...@@ -394,7 +409,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -394,7 +409,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
if (!sampleHolder.decodeOnly) { if (!sampleHolder.decodeOnly) {
currentPositionUs = sampleHolder.timeUs; currentPositionUs = sampleHolder.timeUs;
} }
codecCounters.discardedSamplesCount++;
} else if (result == SampleSource.FORMAT_READ) { } else if (result == SampleSource.FORMAT_READ) {
onInputFormatChanged(formatHolder); onInputFormatChanged(formatHolder);
} }
...@@ -467,7 +481,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -467,7 +481,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
if (result == SampleSource.NOTHING_READ) { if (result == SampleSource.NOTHING_READ) {
codecCounters.inputBufferWaitingForSampleCount++;
return false; return false;
} }
if (result == SampleSource.DISCONTINUITY_READ) { if (result == SampleSource.DISCONTINUITY_READ) {
...@@ -496,7 +509,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -496,7 +509,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
try { try {
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputIndex = -1; inputIndex = -1;
codecCounters.queuedEndOfStreamCount++;
} catch (CryptoException e) { } catch (CryptoException e) {
notifyCryptoError(e); notifyCryptoError(e);
throw new ExoPlaybackException(e); throw new ExoPlaybackException(e);
...@@ -536,10 +548,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -536,10 +548,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} else { } else {
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0); codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
} }
codecCounters.queuedInputBufferCount++;
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
codecCounters.keyframeCount++;
}
inputIndex = -1; inputIndex = -1;
codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReconfigurationState = RECONFIGURATION_STATE_NONE;
} catch (CryptoException e) { } catch (CryptoException e) {
...@@ -625,7 +633,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -625,7 +633,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @param newFormat The new format. * @param newFormat The new format.
* @return True if the existing instance can be reconfigured. False otherwise. * @return True if the existing instance can be reconfigured. False otherwise.
*/ */
@SuppressWarnings("unused")
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
MediaFormat oldFormat, MediaFormat newFormat) { MediaFormat oldFormat, MediaFormat newFormat) {
return false; return false;
...@@ -639,10 +646,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -639,10 +646,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected boolean isReady() { protected boolean isReady() {
return format != null && !waitingForKeys return format != null && !waitingForKeys
&& ((codec == null && !shouldInitCodec()) // We don't want the codec && (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod());
|| 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
} }
private boolean isWithinHotswapPeriod() { private boolean isWithinHotswapPeriod() {
......
...@@ -235,7 +235,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -235,7 +235,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean isReady() { 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. // Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineUs = -1; joiningDeadlineUs = -1;
return true; return true;
......
...@@ -148,12 +148,25 @@ public class MediaFormat { ...@@ -148,12 +148,25 @@ public class MediaFormat {
if (obj == null || getClass() != obj.getClass()) { if (obj == null || getClass() != obj.getClass()) {
return false; return false;
} }
MediaFormat other = (MediaFormat) obj; return equalsInternal((MediaFormat) obj, false);
if (maxInputSize != other.maxInputSize || width != other.width || height != other.height || }
maxWidth != other.maxWidth || maxHeight != other.maxHeight ||
channelCount != other.channelCount || sampleRate != other.sampleRate || public boolean equals(MediaFormat other, boolean ignoreMaxDimensions) {
!Util.areEqual(mimeType, other.mimeType) || if (this == other) {
initializationData.size() != other.initializationData.size()) { 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; return false;
} }
for (int i = 0; i < initializationData.size(); i++) { for (int i = 0; i < initializationData.size(); i++) {
......
...@@ -102,8 +102,11 @@ public interface SampleSource { ...@@ -102,8 +102,11 @@ public interface SampleSource {
* Indicates to the source that it should still be buffering data. * Indicates to the source that it should still be buffering data.
* *
* @param playbackPositionUs The current playback position. * @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. * Attempts to read either a sample, a new format or or a discontinuity from the source.
...@@ -144,8 +147,8 @@ public interface SampleSource { ...@@ -144,8 +147,8 @@ public interface SampleSource {
* This method should not be called until after the source has been successfully prepared. * This method should not be called until after the source has been successfully prepared.
* *
* @return An estimate of the absolute position in micro-seconds up to which data is buffered, * @return An estimate of the absolute position in 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 * or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or
* {@link TrackRenderer#UNKNOWN_TIME} if no estimate is available. * {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
*/ */
public long getBufferedPositionUs(); public long getBufferedPositionUs();
......
...@@ -67,16 +67,16 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -67,16 +67,16 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
/** /**
* Represents an unknown time or duration. * 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 * Represents a time or duration that should match the duration of the longest track whose
* duration is known. * 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. * 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; private int state;
...@@ -110,7 +110,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -110,7 +110,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* *
* @return The current state (one of the STATE_* constants), for convenience. * @return The current state (one of the STATE_* constants), for convenience.
*/ */
@SuppressWarnings("unused")
/* package */ final int prepare() throws ExoPlaybackException { /* package */ final int prepare() throws ExoPlaybackException {
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED); Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
state = doPrepare(); state = doPrepare();
...@@ -301,9 +300,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -301,9 +300,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method may be called when the renderer is in the following states: * This method may be called when the renderer is in the following states:
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} * {@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 * 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(); protected abstract long getDurationUs();
...@@ -324,8 +323,8 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -324,8 +323,8 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* {@link #STATE_ENABLED}, {@link #STATE_STARTED} * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
* *
* @return An estimate of the absolute position in micro-seconds up to which data is buffered, * @return An estimate of the absolute position in 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 * or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if
* estimate is available. * no estimate is available.
*/ */
protected abstract long getBufferedPositionUs(); protected abstract long getBufferedPositionUs();
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.chunk; 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.Allocation;
import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
...@@ -51,7 +52,7 @@ public abstract class Chunk implements Loadable { ...@@ -51,7 +52,7 @@ public abstract class Chunk implements Loadable {
/** /**
* @param dataSource The source from which the data should be loaded. * @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 * @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 * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}. * {@link Integer#MAX_VALUE}.
* @param format See {@link #format}. * @param format See {@link #format}.
...@@ -89,8 +90,8 @@ public abstract class Chunk implements Loadable { ...@@ -89,8 +90,8 @@ public abstract class Chunk implements Loadable {
/** /**
* Gets the length of the chunk in bytes. * Gets the length of the chunk in bytes.
* *
* @return The length of the chunk in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length * @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet
* has yet to be determined. * to be determined.
*/ */
public final long getLength() { public final long getLength() {
return dataSourceStream.getLength(); return dataSourceStream.getLength();
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.FormatHolder;
import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
...@@ -22,7 +23,6 @@ import com.google.android.exoplayer.SampleHolder; ...@@ -22,7 +23,6 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; 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.upstream.Loader;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -57,24 +57,27 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -57,24 +57,27 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* load is for initialization data. * load is for initialization data.
* @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this * @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
* load is for initialization data. * 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, 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. * Invoked when the current load operation completes.
* *
* @param sourceId The id of the reporting {@link SampleSource}. * @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. * Invoked when the current upstream load operation is canceled.
* *
* @param sourceId The id of the reporting {@link SampleSource}. * @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 * 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 { ...@@ -83,10 +86,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* @param sourceId The id of the reporting {@link SampleSource}. * @param sourceId The id of the reporting {@link SampleSource}.
* @param mediaStartTimeMs The media time of the start of the discarded data. * @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 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, void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long totalBytes); long bytesDiscarded);
/** /**
* Invoked when an error occurs loading media data. * Invoked when an error occurs loading media data.
...@@ -111,10 +114,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -111,10 +114,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* @param sourceId The id of the reporting {@link SampleSource}. * @param sourceId The id of the reporting {@link SampleSource}.
* @param mediaStartTimeMs The media time of the start of the discarded data. * @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 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, 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 * 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 { ...@@ -246,11 +249,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { public boolean continueBuffering(long playbackPositionUs) throws IOException {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
downstreamPositionUs = playbackPositionUs; downstreamPositionUs = playbackPositionUs;
chunkSource.continueBuffering(playbackPositionUs); chunkSource.continueBuffering(playbackPositionUs);
updateLoadControl(); 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 @Override
...@@ -309,7 +322,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -309,7 +322,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
} }
MediaFormat mediaFormat = mediaChunk.getMediaFormat(); MediaFormat mediaFormat = mediaChunk.getMediaFormat();
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) { if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat); chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat; formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo(); formatHolder.drmInitData = mediaChunk.getPsshInfo();
...@@ -373,14 +386,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -373,14 +386,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
if (currentLoadable != null && mediaChunk == currentLoadable) { if (currentLoadable != null && mediaChunk == currentLoadable) {
// Linearly interpolate partially-fetched chunk times. // Linearly interpolate partially-fetched chunk times.
long chunkLength = mediaChunk.getLength(); long chunkLength = mediaChunk.getLength();
if (chunkLength != DataSpec.LENGTH_UNBOUNDED) { if (chunkLength != C.LENGTH_UNBOUNDED) {
return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) * return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
mediaChunk.bytesLoaded()) / chunkLength; mediaChunk.bytesLoaded()) / chunkLength;
} else { } else {
return mediaChunk.startTimeUs; return mediaChunk.startTimeUs;
} }
} else if (mediaChunk.isLastChunk()) { } else if (mediaChunk.isLastChunk()) {
return TrackRenderer.END_OF_TRACK; return TrackRenderer.END_OF_TRACK_US;
} else { } else {
return mediaChunk.endTimeUs; return mediaChunk.endTimeUs;
} }
...@@ -399,6 +412,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -399,6 +412,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
@Override @Override
public void onLoaded() { public void onLoaded() {
Chunk currentLoadable = currentLoadableHolder.chunk; Chunk currentLoadable = currentLoadableHolder.chunk;
notifyLoadCompleted(currentLoadable.bytesLoaded());
try { try {
currentLoadable.consume(); currentLoadable.consume();
} catch (IOException e) { } catch (IOException e) {
...@@ -414,7 +428,6 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -414,7 +428,6 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
if (!currentLoadableExceptionFatal) { if (!currentLoadableExceptionFatal) {
clearCurrentLoadable(); clearCurrentLoadable();
} }
notifyLoadCompleted();
updateLoadControl(); updateLoadControl();
} }
} }
...@@ -422,11 +435,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -422,11 +435,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
@Override @Override
public void onCanceled() { public void onCanceled() {
Chunk currentLoadable = currentLoadableHolder.chunk; Chunk currentLoadable = currentLoadableHolder.chunk;
notifyLoadCanceled(currentLoadable.bytesLoaded());
if (!isMediaChunk(currentLoadable)) { if (!isMediaChunk(currentLoadable)) {
currentLoadable.release(); currentLoadable.release();
} }
clearCurrentLoadable(); clearCurrentLoadable();
notifyLoadCanceled();
if (state == STATE_ENABLED) { if (state == STATE_ENABLED) {
restartFrom(pendingResetTime); restartFrom(pendingResetTime);
} else { } else {
...@@ -667,35 +680,35 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -667,35 +680,35 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
private void notifyLoadStarted(final String formatId, final int trigger, private void notifyLoadStarted(final String formatId, final int trigger,
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs, final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
final long totalBytes) { final long length) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization, 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) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onLoadCompleted(eventSourceId); eventListener.onLoadCompleted(eventSourceId, bytesLoaded);
} }
}); });
} }
} }
private void notifyLoadCanceled() { private void notifyLoadCanceled(final long bytesLoaded) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onLoadCanceled(eventSourceId); eventListener.onLoadCanceled(eventSourceId, bytesLoaded);
} }
}); });
} }
...@@ -750,13 +763,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -750,13 +763,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
} }
private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs, private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
final long totalBytes) { final long bytesDiscarded) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
usToMs(mediaEndTimeUs), totalBytes); usToMs(mediaEndTimeUs), bytesDiscarded);
} }
}); });
} }
......
...@@ -72,6 +72,14 @@ public class Format { ...@@ -72,6 +72,14 @@ public class Format {
public final int bitrate; public final int bitrate;
/** /**
* The language of the format. Can be null if unknown.
* <p>
* 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. * The average bandwidth in bytes per second.
* *
* @deprecated Use {@link #bitrate}. However note that the units of measurement are different. * @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
...@@ -90,6 +98,21 @@ public class Format { ...@@ -90,6 +98,21 @@ public class Format {
*/ */
public Format(String id, String mimeType, int width, int height, int numChannels, public Format(String id, String mimeType, int width, int height, int numChannels,
int audioSamplingRate, int bitrate) { 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.id = Assertions.checkNotNull(id);
this.mimeType = mimeType; this.mimeType = mimeType;
this.width = width; this.width = width;
...@@ -97,6 +120,7 @@ public class Format { ...@@ -97,6 +120,7 @@ public class Format {
this.numChannels = numChannels; this.numChannels = numChannels;
this.audioSamplingRate = audioSamplingRate; this.audioSamplingRate = audioSamplingRate;
this.bitrate = bitrate; this.bitrate = bitrate;
this.language = language;
this.bandwidth = bitrate / 8; this.bandwidth = bitrate / 8;
} }
......
...@@ -164,7 +164,7 @@ public interface FormatEvaluator { ...@@ -164,7 +164,7 @@ public interface FormatEvaluator {
*/ */
public static class AdaptiveEvaluator implements 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_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
...@@ -173,7 +173,7 @@ public interface FormatEvaluator { ...@@ -173,7 +173,7 @@ public interface FormatEvaluator {
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final int maxInitialByteRate; private final int maxInitialBitrate;
private final long minDurationForQualityIncreaseUs; private final long minDurationForQualityIncreaseUs;
private final long maxDurationForQualityDecreaseUs; private final long maxDurationForQualityDecreaseUs;
private final long minDurationToRetainAfterDiscardUs; private final long minDurationToRetainAfterDiscardUs;
...@@ -183,7 +183,7 @@ public interface FormatEvaluator { ...@@ -183,7 +183,7 @@ public interface FormatEvaluator {
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
*/ */
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) { 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_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
...@@ -191,7 +191,7 @@ public interface FormatEvaluator { ...@@ -191,7 +191,7 @@ public interface FormatEvaluator {
/** /**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @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. * when bandwidthMeter cannot provide an estimate due to playback having only just started.
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
* the evaluator to consider switching to a higher quality format. * the evaluator to consider switching to a higher quality format.
...@@ -206,13 +206,13 @@ public interface FormatEvaluator { ...@@ -206,13 +206,13 @@ public interface FormatEvaluator {
* for inaccuracies in the bandwidth estimator. * for inaccuracies in the bandwidth estimator.
*/ */
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter, public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
int maxInitialByteRate, int maxInitialBitrate,
int minDurationForQualityIncreaseMs, int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs, int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs, int minDurationToRetainAfterDiscardMs,
float bandwidthFraction) { float bandwidthFraction) {
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
this.maxInitialByteRate = maxInitialByteRate; this.maxInitialBitrate = maxInitialBitrate;
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
...@@ -235,7 +235,7 @@ public interface FormatEvaluator { ...@@ -235,7 +235,7 @@ public interface FormatEvaluator {
long bufferedDurationUs = queue.isEmpty() ? 0 long bufferedDurationUs = queue.isEmpty() ? 0
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
Format current = evaluation.format; 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 isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) { if (isHigher) {
...@@ -276,11 +276,11 @@ public interface FormatEvaluator { ...@@ -276,11 +276,11 @@ public interface FormatEvaluator {
/** /**
* Compute the ideal format ignoring buffer health. * Compute the ideal format ignoring buffer health.
*/ */
protected Format determineIdealFormat(Format[] formats, long bandwidthEstimate) { protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate); long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate);
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
Format format = formats[i]; Format format = formats[i];
if ((format.bitrate / 8) <= effectiveBandwidth) { if (format.bitrate <= effectiveBitrate) {
return format; return format;
} }
} }
...@@ -291,9 +291,9 @@ public interface FormatEvaluator { ...@@ -291,9 +291,9 @@ public interface FormatEvaluator {
/** /**
* Apply overhead factor, or default value in absence of estimate. * Apply overhead factor, or default value in absence of estimate.
*/ */
protected long computeEffectiveBandwidthEstimate(long bandwidthEstimate) { protected long computeEffectiveBitrateEstimate(long bitrateEstimate) {
return bandwidthEstimate == BandwidthMeter.NO_ESTIMATE return bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialByteRate : (long) (bandwidthEstimate * bandwidthFraction); ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
} }
} }
......
...@@ -100,6 +100,14 @@ public abstract class MediaChunk extends Chunk { ...@@ -100,6 +100,14 @@ public abstract class MediaChunk extends Chunk {
public abstract boolean prepare() throws ParserException; 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. * Reads the next media sample from the chunk.
* <p> * <p>
* Should only be called after the chunk has been successfully prepared. * Should only be called after the chunk has been successfully prepared.
......
...@@ -104,11 +104,18 @@ public final class Mp4MediaChunk extends MediaChunk { ...@@ -104,11 +104,18 @@ public final class Mp4MediaChunk extends MediaChunk {
} }
@Override @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 { public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder); 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) { if (sampleRead) {
holder.timeUs -= sampleOffsetUs; holder.timeUs -= sampleOffsetUs;
} }
......
...@@ -83,10 +83,15 @@ public class SingleSampleMediaChunk extends MediaChunk { ...@@ -83,10 +83,15 @@ public class SingleSampleMediaChunk extends MediaChunk {
} }
@Override @Override
public boolean sampleAvailable() {
return isLoadFinished() && !isReadFinished();
}
@Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
if (!isLoadFinished()) { if (!sampleAvailable()) {
return false; return false;
} }
int bytesLoaded = (int) bytesLoaded(); int bytesLoaded = (int) bytesLoaded();
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.webm.WebmExtractor; import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
...@@ -70,10 +71,18 @@ public final class WebmMediaChunk extends MediaChunk { ...@@ -70,10 +71,18 @@ public final class WebmMediaChunk extends MediaChunk {
} }
@Override @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) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
return extractor.read(inputStream, holder); int result = extractor.read(inputStream, holder);
return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
} }
@Override @Override
......
...@@ -146,7 +146,7 @@ public class DashMp4ChunkSource implements ChunkSource { ...@@ -146,7 +146,7 @@ public class DashMp4ChunkSource implements ChunkSource {
RangedUri pendingInitializationUri = null; RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null; RangedUri pendingIndexUri = null;
if (extractor.getTrack() == null) { if (extractor.getFormat() == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri(); pendingInitializationUri = selectedRepresentation.getInitializationUri();
} }
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
...@@ -199,10 +199,10 @@ public class DashMp4ChunkSource implements ChunkSource { ...@@ -199,10 +199,10 @@ public class DashMp4ChunkSource implements ChunkSource {
if (initializationUri != null) { if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge // It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once. // the two requests together to request both at once.
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV; expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri); requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) { if (requestUri != null) {
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
indexAnchor = indexUri.start + indexUri.length; indexAnchor = indexUri.start + indexUri.length;
} else { } else {
requestUri = initializationUri; requestUri = initializationUri;
...@@ -210,7 +210,7 @@ public class DashMp4ChunkSource implements ChunkSource { ...@@ -210,7 +210,7 @@ public class DashMp4ChunkSource implements ChunkSource {
} else { } else {
requestUri = indexUri; requestUri = indexUri;
indexAnchor = indexUri.start + indexUri.length; 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, DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey()); representation.getCacheKey());
...@@ -256,9 +256,9 @@ public class DashMp4ChunkSource implements ChunkSource { ...@@ -256,9 +256,9 @@ public class DashMp4ChunkSource implements ChunkSource {
throw new ParserException("Invalid extractor result. Expected " throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result); + expectedExtractorResult + ", got " + result);
} }
if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) { if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id, segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor)); new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
} }
} }
......
...@@ -56,21 +56,26 @@ public class DashWebmChunkSource implements ChunkSource { ...@@ -56,21 +56,26 @@ public class DashWebmChunkSource implements ChunkSource {
private final Format[] formats; private final Format[] formats;
private final HashMap<String, Representation> representations; private final HashMap<String, Representation> representations;
private final HashMap<String, WebmExtractor> extractors; private final HashMap<String, DefaultWebmExtractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes; private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization; 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, public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
Representation... representations) { Representation... representations) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.evaluator = evaluator; this.evaluator = evaluator;
this.formats = new Format[representations.length]; this.formats = new Format[representations.length];
this.extractors = new HashMap<String, WebmExtractor>(); this.extractors = new HashMap<String, DefaultWebmExtractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>(); this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>(); this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo( this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].format.mimeType, representations[0].periodDurationMs * 1000); representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
...@@ -109,7 +114,7 @@ public class DashWebmChunkSource implements ChunkSource { ...@@ -109,7 +114,7 @@ public class DashWebmChunkSource implements ChunkSource {
@Override @Override
public void disable(List<? extends MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
evaluator.disable(); evaluator.disable();
} }
@Override @Override
...@@ -140,13 +145,18 @@ public class DashWebmChunkSource implements ChunkSource { ...@@ -140,13 +145,18 @@ public class DashWebmChunkSource implements ChunkSource {
Representation selectedRepresentation = representations.get(selectedFormat.id); Representation selectedRepresentation = representations.get(selectedFormat.id);
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
if (!extractor.isPrepared()) { RangedUri pendingInitializationUri = null;
// TODO: This code forces cues to exist and to immediately follow the initialization RangedUri pendingIndexUri = null;
// data. Webm extractor should be generalized to allow cues to be optional. See [redacted]. if (extractor.getFormat() == null) {
RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge( pendingInitializationUri = selectedRepresentation.getInitializationUri();
selectedRepresentation.getIndexUri()); }
Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation, if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
extractor, dataSource, evaluation.trigger); 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; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
...@@ -181,12 +191,29 @@ public class DashWebmChunkSource implements ChunkSource { ...@@ -181,12 +191,29 @@ public class DashWebmChunkSource implements ChunkSource {
// Do nothing. // Do nothing.
} }
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation, private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
WebmExtractor extractor, DataSource dataSource, int trigger) { Representation representation, WebmExtractor extractor, DataSource dataSource,
DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start, int trigger) {
initializationUri.length, representation.getCacheKey()); 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, return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
extractor); extractor, expectedExtractorResult);
} }
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
...@@ -206,22 +233,27 @@ public class DashWebmChunkSource implements ChunkSource { ...@@ -206,22 +233,27 @@ public class DashWebmChunkSource implements ChunkSource {
private class InitializationWebmLoadable extends Chunk { private class InitializationWebmLoadable extends Chunk {
private final WebmExtractor extractor; private final WebmExtractor extractor;
private final int expectedExtractorResult;
private final Uri uri; private final Uri uri;
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, WebmExtractor extractor) { Format format, WebmExtractor extractor, int expectedExtractorResult) {
super(dataSource, dataSpec, format, trigger); super(dataSource, dataSpec, format, trigger);
this.extractor = extractor; this.extractor = extractor;
this.expectedExtractorResult = expectedExtractorResult;
this.uri = dataSpec.uri; this.uri = dataSpec.uri;
} }
@Override @Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException { protected void consumeStream(NonBlockingInputStream stream) throws IOException {
extractor.read(stream, null); int result = extractor.read(stream, null);
if (!extractor.isPrepared()) { if (result != expectedExtractorResult) {
throw new ParserException("Invalid initialization data"); 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));
} }
} }
......
...@@ -140,6 +140,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { ...@@ -140,6 +140,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String mimeType = xpp.getAttributeValue(null, "mimeType"); String mimeType = xpp.getAttributeValue(null, "mimeType");
String language = xpp.getAttributeValue(null, "lang");
int contentType = parseAdaptationSetTypeFromMimeType(mimeType); int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
int id = -1; int id = -1;
...@@ -160,7 +161,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { ...@@ -160,7 +161,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
} else if (isStartTag(xpp, "Representation")) { } else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs, Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
periodDurationMs, mimeType, segmentBase); periodDurationMs, mimeType, language, segmentBase);
contentType = checkAdaptationSetTypeConsistency(contentType, contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
representations.add(representation); representations.add(representation);
...@@ -230,8 +231,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { ...@@ -230,8 +231,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
// Representation parsing. // Representation parsing.
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase) long periodStartMs, long periodDurationMs, String mimeType, String language,
throws XmlPullParserException, IOException { SegmentBase segmentBase) throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth"); int bandwidth = parseInt(xpp, "bandwidth");
int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
...@@ -257,7 +258,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { ...@@ -257,7 +258,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
} while (!isEndTag(xpp, "Representation")); } while (!isEndTag(xpp, "Representation"));
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
bandwidth); bandwidth, language);
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format, return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
segmentBase); segmentBase);
} }
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer.parser.mp4; package com.google.android.exoplayer.parser.mp4;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/* package */ abstract class Atom { /* package */ abstract class Atom {
...@@ -24,7 +23,6 @@ import java.util.List; ...@@ -24,7 +23,6 @@ import java.util.List;
public static final int TYPE_avc3 = 0x61766333; public static final int TYPE_avc3 = 0x61766333;
public static final int TYPE_esds = 0x65736473; public static final int TYPE_esds = 0x65736473;
public static final int TYPE_mdat = 0x6D646174; 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_mp4a = 0x6D703461;
public static final int TYPE_tfdt = 0x74666474; public static final int TYPE_tfdt = 0x74666474;
public static final int TYPE_tfhd = 0x74666864; public static final int TYPE_tfhd = 0x74666864;
...@@ -54,6 +52,7 @@ import java.util.List; ...@@ -54,6 +52,7 @@ import java.util.List;
public static final int TYPE_frma = 0x66726D61; public static final int TYPE_frma = 0x66726D61;
public static final int TYPE_saiz = 0x7361697A; public static final int TYPE_saiz = 0x7361697A;
public static final int TYPE_uuid = 0x75756964; public static final int TYPE_uuid = 0x75756964;
public static final int TYPE_senc = 0x73656E63;
public final int type; public final int type;
...@@ -63,17 +62,13 @@ import java.util.List; ...@@ -63,17 +62,13 @@ import java.util.List;
public final static class LeafAtom extends Atom { public final static class LeafAtom extends Atom {
private final ParsableByteArray data; public final ParsableByteArray data;
public LeafAtom(int type, ParsableByteArray data) { public LeafAtom(int type, ParsableByteArray data) {
super(type); super(type);
this.data = data; this.data = data;
} }
public ParsableByteArray getData() {
return data;
}
} }
public final static class ContainerAtom extends Atom { public final static class ContainerAtom extends Atom {
...@@ -90,7 +85,8 @@ import java.util.List; ...@@ -90,7 +85,8 @@ import java.util.List;
} }
public LeafAtom getLeafAtomOfType(int type) { 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); Atom atom = children.get(i);
if (atom.type == type) { if (atom.type == type) {
return (LeafAtom) atom; return (LeafAtom) atom;
...@@ -100,7 +96,8 @@ import java.util.List; ...@@ -100,7 +96,8 @@ import java.util.List;
} }
public ContainerAtom getContainerAtomOfType(int type) { 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); Atom atom = children.get(i);
if (atom.type == type) { if (atom.type == type) {
return (ContainerAtom) atom; return (ContainerAtom) atom;
...@@ -109,10 +106,6 @@ import java.util.List; ...@@ -109,10 +106,6 @@ import java.util.List;
return null; return null;
} }
public List<Atom> getChildren() {
return children;
}
} }
} }
...@@ -27,7 +27,7 @@ import java.util.List; ...@@ -27,7 +27,7 @@ import java.util.List;
/** /**
* Provides static utility methods for manipulating various types of codec specific data. * 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}; private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
......
...@@ -23,17 +23,14 @@ import java.nio.ByteBuffer; ...@@ -23,17 +23,14 @@ import java.nio.ByteBuffer;
*/ */
/* package */ final class ParsableByteArray { /* package */ final class ParsableByteArray {
private final byte[] data; public byte[] data;
private int position; private int position;
public ParsableByteArray(int length) { public ParsableByteArray(int length) {
this.data = new byte[length]; this.data = new byte[length];
} }
public byte[] getData() {
return data;
}
public int length() { public int length() {
return data.length; return data.length;
} }
......
...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.parser.mp4; ...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.parser.mp4;
/** /**
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream. * 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. * Indicates the encryption state of the samples in the sample group.
......
...@@ -15,48 +15,136 @@ ...@@ -15,48 +15,136 @@
*/ */
package com.google.android.exoplayer.parser.mp4; 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. * A holder for information corresponding to a single fragment of an mp4 file.
*/ */
/* package */ class TrackFragment { /* package */ final class TrackFragment {
public int sampleDescriptionIndex; public int sampleDescriptionIndex;
/**
* The number of samples contained by the fragment.
*/
public int length; public int length;
/**
* The size of each sample in the run.
*/
public int[] sampleSizeTable; public int[] sampleSizeTable;
/**
* The decoding time of each sample in the run.
*/
public int[] sampleDecodingTimeTable; public int[] sampleDecodingTimeTable;
/**
* The composition time offset of each sample in the run.
*/
public int[] sampleCompositionTimeOffsetTable; public int[] sampleCompositionTimeOffsetTable;
/**
* Indicates which samples are sync frames.
*/
public boolean[] sampleIsSyncFrameTable; 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; * Resets the fragment.
* <p>
public boolean smoothStreamingUsesSubsampleEncryption; * The {@link #length} is set to 0, and both {@link #definesEncryptionData} and
public ParsableByteArray smoothStreamingSampleEncryptionData; * {@link #sampleEncryptionDataNeedsFill} is set to false.
*/
public void reset() {
length = 0;
definesEncryptionData = false;
sampleEncryptionDataNeedsFill = false;
}
public void setSampleDescriptionIndex(int sampleDescriptionIndex) { /**
this.sampleDescriptionIndex = sampleDescriptionIndex; * Configures the fragment for the specified number of samples.
* <p>
* 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 setSampleTables(int[] sampleSizeTable, int[] sampleDecodingTimeTable, /**
int[] sampleCompositionTimeOffsetTable, boolean[] sampleIsSyncFrameTable) { * Configures the fragment to be one that defines encryption data of the specified length.
this.sampleSizeTable = sampleSizeTable; * <p>
this.sampleDecodingTimeTable = sampleDecodingTimeTable; * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to
this.sampleCompositionTimeOffsetTable = sampleCompositionTimeOffsetTable; * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it
this.sampleIsSyncFrameTable = sampleIsSyncFrameTable; * is at least this length.
this.length = sampleSizeTable.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 setAuxiliarySampleInfoTables(int totalAuxiliarySampleInfoSize, /**
int[] auxiliarySampleInfoSizeTable) { * Fills {@link #sampleEncryptionData} from the provided source.
this.auxiliarySampleInfoTotalSize = totalAuxiliarySampleInfoSize; *
this.auxiliarySampleInfoSizeTable = auxiliarySampleInfoSizeTable; * @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;
} }
public void setSmoothStreamingSampleEncryptionData(ParsableByteArray data, /**
boolean usesSubsampleEncryption) { * Fills {@link #sampleEncryptionData} for the current run from the provided source.
this.smoothStreamingSampleEncryptionData = data; *
this.smoothStreamingUsesSubsampleEncryption = usesSubsampleEncryption; * @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) { public int getSamplePresentationTime(int index) {
......
...@@ -138,9 +138,8 @@ import java.util.Stack; ...@@ -138,9 +138,8 @@ import java.util.Stack;
while (true) { while (true) {
while (!masterElementsStack.isEmpty() while (!masterElementsStack.isEmpty()
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) { && bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) { eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId);
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
} }
if (state == STATE_BEGIN_READING) { if (state == STATE_BEGIN_READING) {
...@@ -161,12 +160,10 @@ import java.util.Stack; ...@@ -161,12 +160,10 @@ import java.util.Stack;
case TYPE_MASTER: case TYPE_MASTER:
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize)); masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
if (!eventHandler.onMasterElementStart( eventHandler.onMasterElementStart(elementId, elementOffset, masterHeaderSize,
elementId, elementOffset, masterHeaderSize, elementContentSize)) { elementContentSize);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_UNSIGNED_INT: case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size " + elementContentSize); throw new IllegalStateException("Invalid integer size " + elementContentSize);
...@@ -177,11 +174,9 @@ import java.util.Stack; ...@@ -177,11 +174,9 @@ import java.util.Stack;
return intResult; return intResult;
} }
long intValue = getTempByteArrayValue((int) elementContentSize, false); long intValue = getTempByteArrayValue((int) elementContentSize, false);
if (!eventHandler.onIntegerElement(elementId, intValue)) { eventHandler.onIntegerElement(elementId, intValue);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_FLOAT: case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
...@@ -199,11 +194,9 @@ import java.util.Stack; ...@@ -199,11 +194,9 @@ import java.util.Stack;
} else { } else {
floatValue = Double.longBitsToDouble(valueBits); floatValue = Double.longBitsToDouble(valueBits);
} }
if (!eventHandler.onFloatElement(elementId, floatValue)) { eventHandler.onFloatElement(elementId, floatValue);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_STRING: case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) { if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException( throw new IllegalStateException(
...@@ -219,11 +212,9 @@ import java.util.Stack; ...@@ -219,11 +212,9 @@ import java.util.Stack;
} }
String stringValue = new String(stringBytes, Charset.forName("UTF-8")); String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
stringBytes = null; stringBytes = null;
if (!eventHandler.onStringElement(elementId, stringValue)) { eventHandler.onStringElement(elementId, stringValue);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_BINARY: case TYPE_BINARY:
if (elementContentSize > Integer.MAX_VALUE) { if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException( throw new IllegalStateException(
...@@ -233,18 +224,17 @@ import java.util.Stack; ...@@ -233,18 +224,17 @@ import java.util.Stack;
return READ_RESULT_NEED_MORE_DATA; return READ_RESULT_NEED_MORE_DATA;
} }
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. 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); elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize; if (consumed) {
if (expectedBytesRead != bytesRead) { long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
throw new IllegalStateException("Incorrect total bytes read. Expected " if (expectedBytesRead != bytesRead) {
+ expectedBytesRead + " but actually " + bytesRead); throw new IllegalStateException("Incorrect total bytes read. Expected "
} + expectedBytesRead + " but actually " + bytesRead);
if (!keepGoing) { }
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE;
} }
break; return READ_RESULT_CONTINUE;
case TYPE_UNKNOWN: case TYPE_UNKNOWN:
if (elementContentSize > Integer.MAX_VALUE) { if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException( throw new IllegalStateException(
...@@ -254,11 +244,11 @@ import java.util.Stack; ...@@ -254,11 +244,11 @@ import java.util.Stack;
if (skipResult != READ_RESULT_CONTINUE) { if (skipResult != READ_RESULT_CONTINUE) {
return skipResult; return skipResult;
} }
prepareForNextElement();
break; break;
default: default:
throw new IllegalStateException("Invalid element type " + type); throw new IllegalStateException("Invalid element type " + type);
} }
prepareForNextElement();
} }
} }
...@@ -508,7 +498,7 @@ import java.util.Stack; ...@@ -508,7 +498,7 @@ import java.util.Stack;
*/ */
private int updateBytesState(int additionalBytesRead, int totalBytes) { private int updateBytesState(int additionalBytesRead, int totalBytes) {
if (additionalBytesRead == -1) { if (additionalBytesRead == -1) {
return READ_RESULT_END_OF_FILE; return READ_RESULT_END_OF_STREAM;
} }
bytesState += additionalBytesRead; bytesState += additionalBytesRead;
bytesRead += additionalBytesRead; bytesRead += additionalBytesRead;
......
...@@ -46,9 +46,8 @@ import java.nio.ByteBuffer; ...@@ -46,9 +46,8 @@ import java.nio.ByteBuffer;
* @param elementOffsetBytes The byte offset where this element starts * @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header * @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's children * @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); int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
/** /**
...@@ -56,44 +55,42 @@ import java.nio.ByteBuffer; ...@@ -56,44 +55,42 @@ import java.nio.ByteBuffer;
* {@link NonBlockingInputStream}. * {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @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}. * Called when an integer element is encountered in the {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @param value The integer value this element contains * @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}. * Called when a float element is encountered in the {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @param value The float value this element contains * @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}. * Called when a string element is encountered in the {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @param value The string value this element contains * @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}. * Called when a binary element is encountered in the {@link NonBlockingInputStream}.
* *
* <p>The element header (containing element ID and content size) will already have been read. * <p>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 * Subclasses must either read nothing and return {@code false}, or exactly read the entire
* {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be * contents of the element, which is {@code contentsSizeBytes} in length, and return {@code true}.
* immediately available from {@code inputStream}. *
* <p>It's guaranteed that the full element contents will be immediately available from
* {@code inputStream}.
* *
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a * <p>Several methods in {@link EbmlReader} are available for reading the contents of a
* binary element: * binary element:
...@@ -111,7 +108,7 @@ import java.nio.ByteBuffer; ...@@ -111,7 +108,7 @@ import java.nio.ByteBuffer;
* @param contentsSizeBytes The byte length of this element's contents * @param contentsSizeBytes The byte length of this element's contents
* @param inputStream The {@link NonBlockingInputStream} from which this * @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read * 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( public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
......
...@@ -44,12 +44,12 @@ import java.nio.ByteBuffer; ...@@ -44,12 +44,12 @@ import java.nio.ByteBuffer;
// Return values for reading methods. // Return values for reading methods.
public static final int READ_RESULT_CONTINUE = 0; public static final int READ_RESULT_CONTINUE = 0;
public static final int READ_RESULT_NEED_MORE_DATA = 1; 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); 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 * @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this interface * @return One of the {@code RESULT_*} flags defined in this interface
......
...@@ -30,24 +30,38 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; ...@@ -30,24 +30,38 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
public interface WebmExtractor { public interface WebmExtractor {
/** /**
* Whether the has parsed the cues and sample format from the stream. * An attempt to read from the input stream returned insufficient data.
* */
* @return True if the extractor is prepared. False otherwise 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 boolean isPrepared(); 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}. * Consumes data from a {@link NonBlockingInputStream}.
* *
* <p>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 inputStream The input stream from which data should be read
* @param sampleHolder A {@link SampleHolder} into which the sample 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. * Seeks to a position before or equal to the requested time.
...@@ -66,7 +80,7 @@ public interface WebmExtractor { ...@@ -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 * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
* prepared * prepared
*/ */
public SegmentIndex getCues(); public SegmentIndex getIndex();
/** /**
* Returns the format of the samples contained within the media stream. * Returns the format of the samples contained within the media stream.
......
...@@ -144,7 +144,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { ...@@ -144,7 +144,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
@Override @Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException { protected void doSomeWork(long timeUs) throws ExoPlaybackException {
source.continueBuffering(timeUs); try {
source.continueBuffering(timeUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
currentPositionUs = timeUs; currentPositionUs = timeUs;
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance // 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 { ...@@ -225,7 +230,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
@Override @Override
protected long getBufferedPositionUs() { protected long getBufferedPositionUs() {
// Don't block playback whilst subtitles are loading. // Don't block playback whilst subtitles are loading.
return END_OF_TRACK; return END_OF_TRACK_US;
} }
@Override @Override
...@@ -275,7 +280,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { ...@@ -275,7 +280,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
} }
} }
@SuppressWarnings("unchecked")
@Override @Override
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
......
...@@ -25,6 +25,28 @@ package com.google.android.exoplayer.upstream; ...@@ -25,6 +25,28 @@ package com.google.android.exoplayer.upstream;
public interface Allocation { public interface Allocation {
/** /**
* Ensures the allocation has a capacity greater than or equal to the specified size in bytes.
* <p>
* 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.
* <p>
* 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. * Gets the buffers in which the fragments are allocated.
* *
* @return The buffers in which the fragments are allocated. * @return The buffers in which the fragments are allocated.
......
...@@ -26,10 +26,10 @@ public interface BandwidthMeter { ...@@ -26,10 +26,10 @@ public interface BandwidthMeter {
final long NO_ESTIMATE = -1; 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();
} }
...@@ -67,15 +67,39 @@ public final class BufferPool implements Allocator { ...@@ -67,15 +67,39 @@ public final class BufferPool implements Allocator {
@Override @Override
public synchronized Allocation allocate(int size) { public synchronized Allocation allocate(int size) {
return new AllocationImpl(allocate(size, null));
}
/**
* Allocates byte arrays whose combined length is at least {@code size}.
* <p>
* 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); 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][]; 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. // Use a recycled buffer if one is available. Else instantiate a new one.
buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] : buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] :
new byte[bufferLength]; new byte[bufferLength];
} }
return new AllocationImpl(buffers); return buffers;
} }
/** /**
...@@ -113,6 +137,16 @@ public final class BufferPool implements Allocator { ...@@ -113,6 +137,16 @@ public final class BufferPool implements Allocator {
} }
@Override @Override
public void ensureCapacity(int size) {
buffers = allocate(size, buffers);
}
@Override
public int capacity() {
return bufferLength * buffers.length;
}
@Override
public byte[][] getBuffers() { public byte[][] getBuffers() {
return buffers; return buffers;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream; 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.Assertions;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
...@@ -29,7 +30,7 @@ public class ByteArrayDataSink implements DataSink { ...@@ -29,7 +30,7 @@ public class ByteArrayDataSink implements DataSink {
@Override @Override
public DataSink open(DataSpec dataSpec) throws IOException { public DataSink open(DataSpec dataSpec) throws IOException {
if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) { if (dataSpec.length == C.LENGTH_UNBOUNDED) {
stream = new ByteArrayOutputStream(); stream = new ByteArrayOutputStream();
} else { } else {
Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE); Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream; 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.Assertions;
import java.io.IOException; import java.io.IOException;
...@@ -36,14 +37,14 @@ public class ByteArrayDataSource implements DataSource { ...@@ -36,14 +37,14 @@ public class ByteArrayDataSource implements DataSource {
@Override @Override
public long open(DataSpec dataSpec) throws IOException { 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); Assertions.checkArgument(dataSpec.position < data.length);
} else { } else {
Assertions.checkArgument(dataSpec.position + dataSpec.length <= data.length); Assertions.checkArgument(dataSpec.position + dataSpec.length <= data.length);
} }
readPosition = (int) dataSpec.position; readPosition = (int) dataSpec.position;
return (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) return (dataSpec.length == C.LENGTH_UNBOUNDED) ? (data.length - dataSpec.position)
? (data.length - dataSpec.position) : dataSpec.length; : dataSpec.length;
} }
@Override @Override
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer.upstream; package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -34,9 +36,10 @@ public interface DataSource { ...@@ -34,9 +36,10 @@ public interface DataSource {
* @param dataSpec Defines the data to be read. * @param dataSpec Defines the data to be read.
* @throws IOException If an error occurs opening the source. * @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 * @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}) * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNBOUNDED}) this value
* this value is the resolved length of the request. For all other requests, the value * is the resolved length of the request, or {@link C#LENGTH_UNBOUNDED} if the length is still
* returned will be equal to the request's {@link DataSpec#length}. * 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; public long open(DataSpec dataSpec) throws IOException;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream; 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.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -39,6 +40,8 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -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 DataSource dataSource;
private final DataSpec dataSpec; private final DataSpec dataSpec;
private final Allocator allocator; private final Allocator allocator;
...@@ -57,7 +60,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -57,7 +60,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
/** /**
* @param dataSource The source from which the data should be loaded. * @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 * @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 * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}. * {@link Integer#MAX_VALUE}.
* @param allocator Used to obtain an {@link Allocation} for holding the data. * @param allocator Used to obtain an {@link Allocation} for holding the data.
...@@ -67,7 +70,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -67,7 +70,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
this.dataSource = dataSource; this.dataSource = dataSource;
this.dataSpec = dataSpec; this.dataSpec = dataSpec;
this.allocator = allocator; this.allocator = allocator;
resolvedLength = DataSpec.LENGTH_UNBOUNDED; resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead(); readHead = new ReadHead();
} }
...@@ -97,13 +100,14 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -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 * @return The length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has
* has yet to be determined. * yet to be determined.
*/ */
public long getLength() { 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 ...@@ -112,7 +116,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
* @return True if the stream has finished loading. False otherwise. * @return True if the stream has finished loading. False otherwise.
*/ */
public boolean isLoadFinished() { 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 ...@@ -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 * 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. * 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() { public final byte[] getLoadedData() {
if (loadPosition == 0) { if (loadPosition == 0) {
...@@ -144,7 +148,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -144,7 +148,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
@Override @Override
public boolean isEndOfStream() { public boolean isEndOfStream() {
return resolvedLength != DataSpec.LENGTH_UNBOUNDED && readHead.position == resolvedLength; return resolvedLength != C.LENGTH_UNBOUNDED && readHead.position == resolvedLength;
} }
@Override @Override
...@@ -191,6 +195,11 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -191,6 +195,11 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
int bytesRead = 0; int bytesRead = 0;
byte[][] buffers = allocation.getBuffers(); byte[][] buffers = allocation.getBuffers();
while (bytesRead < bytesToRead) { 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); int bufferReadLength = Math.min(readHead.fragmentRemaining, bytesToRead - bytesRead);
if (target != null) { if (target != null) {
target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength); target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength);
...@@ -203,11 +212,6 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -203,11 +212,6 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
bytesRead += bufferReadLength; bytesRead += bufferReadLength;
readHead.fragmentOffset += bufferReadLength; readHead.fragmentOffset += bufferReadLength;
readHead.fragmentRemaining -= 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; return bytesRead;
...@@ -231,23 +235,32 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -231,23 +235,32 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
// The load was canceled, or is already complete. // The load was canceled, or is already complete.
return; return;
} }
try { try {
DataSpec loadDataSpec; DataSpec loadDataSpec;
if (resolvedLength == DataSpec.LENGTH_UNBOUNDED) { if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) {
loadDataSpec = dataSpec; loadDataSpec = dataSpec;
resolvedLength = dataSource.open(loadDataSpec); long resolvedLength = dataSource.open(loadDataSpec);
if (resolvedLength > Integer.MAX_VALUE) { if (resolvedLength > Integer.MAX_VALUE) {
throw new DataSourceStreamLoadException( throw new DataSourceStreamLoadException(
new UnexpectedLengthException(dataSpec.length, resolvedLength)); new UnexpectedLengthException(dataSpec.length, resolvedLength));
} }
this.resolvedLength = resolvedLength;
} else { } else {
long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED
? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED;
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
resolvedLength - loadPosition, dataSpec.key); remainingLength, dataSpec.key);
dataSource.open(loadDataSpec); dataSource.open(loadDataSpec);
} }
if (allocation == null) { 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) { if (loadPosition == 0) {
writeFragmentIndex = 0; writeFragmentIndex = 0;
writeFragmentOffset = allocation.getFragmentOffset(0); writeFragmentOffset = allocation.getFragmentOffset(0);
...@@ -256,22 +269,28 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -256,22 +269,28 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
int read = Integer.MAX_VALUE; int read = Integer.MAX_VALUE;
byte[][] buffers = allocation.getBuffers(); byte[][] buffers = allocation.getBuffers();
while (!loadCanceled && loadPosition < resolvedLength && read > 0) { while (!loadCanceled && read > 0 && maybeMoreToLoad()) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw new InterruptedException(); throw new InterruptedException();
} }
int writeLength = (int) Math.min(writeFragmentRemainingLength, read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset,
resolvedLength - loadPosition); writeFragmentRemainingLength);
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, writeLength);
if (read > 0) { if (read > 0) {
loadPosition += read; loadPosition += read;
writeFragmentOffset += read; writeFragmentOffset += read;
writeFragmentRemainingLength -= read; writeFragmentRemainingLength -= read;
if (writeFragmentRemainingLength == 0 && loadPosition < resolvedLength) { if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) {
writeFragmentIndex++; writeFragmentIndex++;
if (loadPosition == allocationCapacity) {
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
}
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex); writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex); writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
} }
} else if (resolvedLength == C.LENGTH_UNBOUNDED) {
resolvedLength = loadPosition;
} else if (resolvedLength != loadPosition) { } else if (resolvedLength != loadPosition) {
throw new DataSourceStreamLoadException( throw new DataSourceStreamLoadException(
new UnexpectedLengthException(resolvedLength, loadPosition)); new UnexpectedLengthException(resolvedLength, loadPosition));
...@@ -282,6 +301,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -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 static class ReadHead {
private int position; private int position;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream; 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.Assertions;
import android.net.Uri; import android.net.Uri;
...@@ -25,13 +26,6 @@ import android.net.Uri; ...@@ -25,13 +26,6 @@ import android.net.Uri;
public final class DataSpec { 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. * Identifies the source from which data should be read.
*/ */
public final Uri uri; public final Uri uri;
...@@ -50,7 +44,7 @@ public final class DataSpec { ...@@ -50,7 +44,7 @@ public final class DataSpec {
*/ */
public final long position; 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; public final long length;
/** /**
...@@ -98,7 +92,7 @@ public final class DataSpec { ...@@ -98,7 +92,7 @@ public final class DataSpec {
boolean uriIsFullStream) { boolean uriIsFullStream) {
Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(absoluteStreamPosition >= 0);
Assertions.checkArgument(position >= 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); Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
this.uri = uri; this.uri = uri;
this.uriIsFullStream = uriIsFullStream; this.uriIsFullStream = uriIsFullStream;
......
...@@ -38,11 +38,11 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { ...@@ -38,11 +38,11 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
* *
* @param elapsedMs The time taken to transfer the bytes, in milliseconds. * @param elapsedMs The time taken to transfer the bytes, in milliseconds.
* @param bytes The number of bytes transferred. * @param bytes The number of bytes transferred.
* @param bandwidthEstimate The estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
* estimate is available. Note that this estimate is typically derived from more information * is available. Note that this estimate is typically derived from more information than
* than {@code bytes} and {@code elapsedMs}. * {@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 { ...@@ -53,9 +53,9 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
private final Clock clock; private final Clock clock;
private final SlidingPercentile slidingPercentile; private final SlidingPercentile slidingPercentile;
private long accumulator; private long bytesAccumulator;
private long startTimeMs; private long startTimeMs;
private long bandwidthEstimate; private long bitrateEstimate;
private int streamCount; private int streamCount;
public DefaultBandwidthMeter() { public DefaultBandwidthMeter() {
...@@ -80,17 +80,12 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { ...@@ -80,17 +80,12 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
this.eventListener = eventListener; this.eventListener = eventListener;
this.clock = clock; this.clock = clock;
this.slidingPercentile = new SlidingPercentile(maxWeight); 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 @Override
public synchronized long getEstimate() { public synchronized long getBitrateEstimate() {
return bandwidthEstimate; return bitrateEstimate;
} }
@Override @Override
...@@ -103,7 +98,7 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { ...@@ -103,7 +98,7 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
@Override @Override
public synchronized void onBytesTransferred(int bytes) { public synchronized void onBytesTransferred(int bytes) {
accumulator += bytes; bytesAccumulator += bytes;
} }
@Override @Override
...@@ -112,32 +107,26 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { ...@@ -112,32 +107,26 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
long nowMs = clock.elapsedRealtime(); long nowMs = clock.elapsedRealtime();
int elapsedMs = (int) (nowMs - startTimeMs); int elapsedMs = (int) (nowMs - startTimeMs);
if (elapsedMs > 0) { if (elapsedMs > 0) {
float bytesPerSecond = accumulator * 1000 / elapsedMs; float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs;
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond); slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond);
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
: (long) bandwidthEstimateFloat; : (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate); notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate);
} }
streamCount--; streamCount--;
if (streamCount > 0) { if (streamCount > 0) {
startTimeMs = nowMs; 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, private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
final long bandwidthEstimate) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate); eventListener.onBandwidthSample(elapsedMs, bytes, bitrate);
} }
}); });
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer.upstream; package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
...@@ -42,8 +44,7 @@ public final class FileDataSource implements DataSource { ...@@ -42,8 +44,7 @@ public final class FileDataSource implements DataSource {
try { try {
file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); file = new RandomAccessFile(dataSpec.uri.getPath(), "r");
file.seek(dataSpec.position); file.seek(dataSpec.position);
bytesRemaining = dataSpec.length == DataSpec.LENGTH_UNBOUNDED bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position
? file.length() - dataSpec.position
: dataSpec.length; : dataSpec.length;
return bytesRemaining; return bytesRemaining;
} catch (IOException e) { } catch (IOException e) {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream; 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.Assertions;
import com.google.android.exoplayer.util.Predicate; import com.google.android.exoplayer.util.Predicate;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -258,16 +259,9 @@ public class HttpDataSource implements DataSource { ...@@ -258,16 +259,9 @@ public class HttpDataSource implements DataSource {
} }
long contentLength = getContentLength(connection); long contentLength = getContentLength(connection);
dataLength = dataSpec.length == DataSpec.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; dataLength = dataSpec.length == C.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);
}
if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED && contentLength != DataSpec.LENGTH_UNBOUNDED if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED
&& contentLength != dataSpec.length) { && contentLength != dataSpec.length) {
// The DataSpec specified a length and we resolved a length from the response headers, but // The DataSpec specified a length and we resolved a length from the response headers, but
// the two lengths do not match. // the two lengths do not match.
...@@ -305,9 +299,9 @@ public class HttpDataSource implements DataSource { ...@@ -305,9 +299,9 @@ public class HttpDataSource implements DataSource {
if (listener != null) { if (listener != null) {
listener.onBytesTransferred(read); 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 // 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), throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
dataSpec); dataSpec);
} }
...@@ -364,14 +358,15 @@ public class HttpDataSource implements DataSource { ...@@ -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 * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
* value is equivalent to {@code dataSpec.length - bytesRead()}, where dataSpec is the * <p>
* {@link DataSpec} that was passed to the most recent call of {@link #open(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() { protected final long bytesRemaining() {
return dataLength - bytesRead; return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead;
} }
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
...@@ -394,14 +389,14 @@ public class HttpDataSource implements DataSource { ...@@ -394,14 +389,14 @@ public class HttpDataSource implements DataSource {
private String buildRangeHeader(DataSpec dataSpec) { private String buildRangeHeader(DataSpec dataSpec) {
String rangeRequest = "bytes=" + dataSpec.position + "-"; String rangeRequest = "bytes=" + dataSpec.position + "-";
if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED) { if (dataSpec.length != C.LENGTH_UNBOUNDED) {
rangeRequest += (dataSpec.position + dataSpec.length - 1); rangeRequest += (dataSpec.position + dataSpec.length - 1);
} }
return rangeRequest; return rangeRequest;
} }
private long getContentLength(HttpURLConnection connection) { private long getContentLength(HttpURLConnection connection) {
long contentLength = DataSpec.LENGTH_UNBOUNDED; long contentLength = C.LENGTH_UNBOUNDED;
String contentLengthHeader = connection.getHeaderField("Content-Length"); String contentLengthHeader = connection.getHeaderField("Content-Length");
if (!TextUtils.isEmpty(contentLengthHeader)) { if (!TextUtils.isEmpty(contentLengthHeader)) {
try { try {
...@@ -435,10 +430,6 @@ public class HttpDataSource implements DataSource { ...@@ -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; return contentLength;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream; 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.Assertions;
import java.io.IOException; import java.io.IOException;
...@@ -39,7 +40,7 @@ public final class TeeDataSource implements DataSource { ...@@ -39,7 +40,7 @@ public final class TeeDataSource implements DataSource {
@Override @Override
public long open(DataSpec dataSpec) throws IOException { public long open(DataSpec dataSpec) throws IOException {
long dataLength = upstream.open(dataSpec); 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. // Reconstruct dataSpec in order to provide the resolved length to the sink.
dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength, dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength,
dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream); dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream);
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream.cache; 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.DataSink;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -63,6 +64,9 @@ public class CacheDataSink implements DataSink { ...@@ -63,6 +64,9 @@ public class CacheDataSink implements DataSink {
@Override @Override
public DataSink open(DataSpec dataSpec) throws CacheDataSinkException { 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 { try {
this.dataSpec = dataSpec; this.dataSpec = dataSpec;
dataSpecBytesWritten = 0; dataSpecBytesWritten = 0;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.upstream.cache; 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.DataSink;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
...@@ -34,10 +35,26 @@ import java.io.IOException; ...@@ -34,10 +35,26 @@ import java.io.IOException;
*/ */
public final class CacheDataSource implements DataSource { 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 Cache cache;
private final DataSource cacheReadDataSource; private final DataSource cacheReadDataSource;
private final DataSource cacheWriteDataSource; private final DataSource cacheWriteDataSource;
private final DataSource upstreamDataSource; private final DataSource upstreamDataSource;
private final EventListener eventListener;
private final boolean blockOnCache; private final boolean blockOnCache;
private final boolean ignoreCacheOnError; private final boolean ignoreCacheOnError;
...@@ -49,6 +66,7 @@ public final class CacheDataSource implements DataSource { ...@@ -49,6 +66,7 @@ public final class CacheDataSource implements DataSource {
private long bytesRemaining; private long bytesRemaining;
private CacheSpan lockedSpan; private CacheSpan lockedSpan;
private boolean ignoreCache; private boolean ignoreCache;
private long totalCachedBytesRead;
/** /**
* Constructs an instance with default {@link DataSource} and {@link DataSink} instances for * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
...@@ -67,7 +85,7 @@ public final class CacheDataSource implements DataSource { ...@@ -67,7 +85,7 @@ public final class CacheDataSource implements DataSource {
public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache, public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache,
boolean ignoreCacheOnError, long maxCacheFileSize) { boolean ignoreCacheOnError, long maxCacheFileSize) {
this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, 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 { ...@@ -84,9 +102,11 @@ public final class CacheDataSource implements DataSource {
* @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If * @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 * 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. * 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, 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.cache = cache;
this.cacheReadDataSource = cacheReadDataSource; this.cacheReadDataSource = cacheReadDataSource;
this.blockOnCache = blockOnCache; this.blockOnCache = blockOnCache;
...@@ -97,6 +117,7 @@ public final class CacheDataSource implements DataSource { ...@@ -97,6 +117,7 @@ public final class CacheDataSource implements DataSource {
} else { } else {
this.cacheWriteDataSource = null; this.cacheWriteDataSource = null;
} }
this.eventListener = eventListener;
} }
@Override @Override
...@@ -104,7 +125,7 @@ public final class CacheDataSource implements DataSource { ...@@ -104,7 +125,7 @@ public final class CacheDataSource implements DataSource {
Assertions.checkState(dataSpec.uriIsFullStream); Assertions.checkState(dataSpec.uriIsFullStream);
// TODO: Support caching for unbounded requests. This requires storing the source length // 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). // 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 { try {
uri = dataSpec.uri; uri = dataSpec.uri;
key = dataSpec.key; key = dataSpec.key;
...@@ -121,10 +142,13 @@ public final class CacheDataSource implements DataSource { ...@@ -121,10 +142,13 @@ public final class CacheDataSource implements DataSource {
@Override @Override
public int read(byte[] buffer, int offset, int max) throws IOException { public int read(byte[] buffer, int offset, int max) throws IOException {
try { try {
int num = currentDataSource.read(buffer, offset, max); int bytesRead = currentDataSource.read(buffer, offset, max);
if (num >= 0) { if (bytesRead >= 0) {
readPosition += num; if (currentDataSource == cacheReadDataSource) {
bytesRemaining -= num; totalCachedBytesRead += bytesRead;
}
readPosition += bytesRead;
bytesRemaining -= bytesRead;
} else { } else {
closeCurrentSource(); closeCurrentSource();
if (bytesRemaining > 0) { if (bytesRemaining > 0) {
...@@ -132,7 +156,7 @@ public final class CacheDataSource implements DataSource { ...@@ -132,7 +156,7 @@ public final class CacheDataSource implements DataSource {
return read(buffer, offset, max); return read(buffer, offset, max);
} }
} }
return num; return bytesRead;
} catch (IOException e) { } catch (IOException e) {
handleBeforeThrow(e); handleBeforeThrow(e);
throw e; throw e;
...@@ -141,6 +165,7 @@ public final class CacheDataSource implements DataSource { ...@@ -141,6 +165,7 @@ public final class CacheDataSource implements DataSource {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
notifyBytesRead();
try { try {
closeCurrentSource(); closeCurrentSource();
} catch (IOException e) { } catch (IOException e) {
...@@ -215,4 +240,11 @@ public final class CacheDataSource implements DataSource { ...@@ -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;
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment