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