Merge pull request #29 from google/dev

Merge 1.0.12 to master
This commit is contained in:
ojw28 2014-08-14 17:04:34 +01:00
commit 79c2f535c6
57 changed files with 1087 additions and 734 deletions

39
.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
# Android generated
bin
gen
lint.xml
# IntelliJ IDEA
.idea
*.iml
*.ipr
*.iws
classes
gen-external-apklibs
# Eclipse
.project
.classpath
.settings
.checkstyle
# Gradle
.gradle
build
out
# Maven
target
release.properties
pom.xml.*
# Ant
ant.properties
local.properties
proguard.cfg
proguard-project.txt
# Other
.DS_Store
dist
tmp

View file

@ -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.

View file

@ -19,7 +19,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.10.+'
classpath 'com.android.tools.build:gradle:0.12.+'
}
}

View file

@ -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"/>

View file

@ -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 + ", " +

View file

@ -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

View file

@ -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.
}

View file

@ -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);
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer;
/**
* Defines constants that are generally useful throughout the library.
*/
public final class C {
/**
* Represents an unbounded length of data.
*/
public static final int LENGTH_UNBOUNDED = -1;
private C() {}
}

View file

@ -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();
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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}

View file

@ -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;
}
}

View file

@ -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;
}
/**

View file

@ -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() {

View file

@ -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;

View file

@ -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++) {

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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);
}
});
}

View file

@ -71,6 +71,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.
*
@ -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;
}

View file

@ -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);
}
}

View file

@ -99,6 +99,14 @@ public abstract class MediaChunk extends Chunk {
*/
public abstract boolean prepare() throws ParserException;
/**
* Returns whether the next sample is available.
*
* @return True if the next sample is available for reading. False otherwise.
* @throws ParserException
*/
public abstract boolean sampleAvailable() throws ParserException;
/**
* Reads the next media sample from the chunk.
* <p>

View file

@ -103,12 +103,19 @@ public final class Mp4MediaChunk extends MediaChunk {
return prepared;
}
@Override
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override
public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder);
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE_FULL) != 0;
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
if (sampleRead) {
holder.timeUs -= sampleOffsetUs;
}

View file

@ -82,11 +82,16 @@ public class SingleSampleMediaChunk extends MediaChunk {
return true;
}
@Override
public boolean sampleAvailable() {
return isLoadFinished() && !isReadFinished();
}
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
if (!isLoadFinished()) {
if (!sampleAvailable()) {
return false;
}
int bytesLoaded = (int) bytesLoaded();

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
@ -69,11 +70,19 @@ public final class WebmMediaChunk extends MediaChunk {
return true;
}
@Override
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
return extractor.read(inputStream, holder);
int result = extractor.read(inputStream, holder);
return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
}
@Override

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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};

View file

@ -59,7 +59,7 @@ public final class FragmentedMp4Extractor {
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
/**
* An attempt to read from the input stream returned 0 bytes of data.
* An attempt to read from the input stream returned insufficient data.
*/
public static final int RESULT_NEED_MORE_DATA = 1;
/**
@ -69,27 +69,23 @@ public final class FragmentedMp4Extractor {
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE_FULL = 4;
public static final int RESULT_READ_SAMPLE = 4;
/**
* A media sample was partially read.
* A moov atom was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}.
*/
public static final int RESULT_READ_SAMPLE_PARTIAL = 8;
public static final int RESULT_READ_INIT = 8;
/**
* A moov atom was read. The parsed data can be read using {@link #getTrack()},
* {@link #getFormat()} and {@link #getPsshInfo}.
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_MOOV = 16;
/**
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
*/
public static final int RESULT_READ_SIDX = 32;
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 64;
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE_FULL | RESULT_NEED_SAMPLE_HOLDER;
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@ -97,9 +93,8 @@ public final class FragmentedMp4Extractor {
// Parser states
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_CENC_AUXILIARY_DATA = 2;
private static final int STATE_READING_SAMPLE_START = 3;
private static final int STATE_READING_SAMPLE_INCREMENTAL = 4;
private static final int STATE_READING_ENCRYPTION_DATA = 2;
private static final int STATE_READING_SAMPLE = 3;
// Atom data offsets
private static final int ATOM_HEADER_SIZE = 8;
@ -115,7 +110,6 @@ public final class FragmentedMp4Extractor {
parsedAtoms.add(Atom.TYPE_hdlr);
parsedAtoms.add(Atom.TYPE_mdat);
parsedAtoms.add(Atom.TYPE_mdhd);
parsedAtoms.add(Atom.TYPE_mfhd);
parsedAtoms.add(Atom.TYPE_moof);
parsedAtoms.add(Atom.TYPE_moov);
parsedAtoms.add(Atom.TYPE_mp4a);
@ -135,6 +129,7 @@ public final class FragmentedMp4Extractor {
parsedAtoms.add(Atom.TYPE_pssh);
parsedAtoms.add(Atom.TYPE_saiz);
parsedAtoms.add(Atom.TYPE_uuid);
parsedAtoms.add(Atom.TYPE_senc);
PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
}
@ -158,8 +153,10 @@ public final class FragmentedMp4Extractor {
// Parser state
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
private final Stack<ContainerAtom> containerAtoms;
private final Stack<Integer> containerAtomEndPoints;
private final TrackFragment fragmentRun;
private int parserState;
private int atomBytesRead;
@ -167,9 +164,6 @@ public final class FragmentedMp4Extractor {
private int atomType;
private int atomSize;
private ParsableByteArray atomData;
private ParsableByteArray cencAuxiliaryData;
private int cencAuxiliaryBytesRead;
private int sampleBytesRead;
private int pendingSeekTimeMs;
private int sampleIndex;
@ -182,9 +176,6 @@ public final class FragmentedMp4Extractor {
private Track track;
private DefaultSampleValues extendsDefaults;
// Data parsed from the most recent moof atom
private TrackFragment fragmentRun;
public FragmentedMp4Extractor() {
this(0);
}
@ -197,8 +188,10 @@ public final class FragmentedMp4Extractor {
this.workaroundFlags = workaroundFlags;
parserState = STATE_READING_ATOM_HEADER;
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<ContainerAtom>();
containerAtomEndPoints = new Stack<Integer>();
fragmentRun = new TrackFragment();
psshData = new HashMap<UUID, byte[]>();
}
@ -207,7 +200,7 @@ public final class FragmentedMp4Extractor {
*
* @return The segment index, or null if a SIDX atom has yet to be parsed.
*/
public SegmentIndex getSegmentIndex() {
public SegmentIndex getIndex() {
return segmentIndex;
}
@ -245,17 +238,7 @@ public final class FragmentedMp4Extractor {
}
/**
* Returns the track information parsed from the stream.
*
* @return The track, or null if a MOOV atom has yet to be parsed.
*/
public Track getTrack() {
return track;
}
/**
* Sideloads track information into the extractor, so that it can be read through
* {@link #getTrack()}.
* Sideloads track information into the extractor.
*
* @param track The track to sideload.
*/
@ -270,10 +253,6 @@ public final class FragmentedMp4Extractor {
* The read terminates if the end of the input stream is reached, if an attempt to read from the
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
* both the reason for termination and data that was parsed during the read.
* <p>
* If the returned flags include {@link #RESULT_READ_SAMPLE_PARTIAL} then the sample has been
* partially read into {@code out}. Hence the same {@link SampleHolder} instance must be passed
* in subsequent calls until the whole sample has been read.
*
* @param inputStream The input stream from which data should be read.
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
@ -293,8 +272,8 @@ public final class FragmentedMp4Extractor {
case STATE_READING_ATOM_PAYLOAD:
results |= readAtomPayload(inputStream);
break;
case STATE_READING_CENC_AUXILIARY_DATA:
results |= readCencAuxiliaryData(inputStream);
case STATE_READING_ENCRYPTION_DATA:
results |= readEncryptionData(inputStream);
break;
default:
results |= readOrSkipSample(inputStream, out);
@ -350,19 +329,13 @@ public final class FragmentedMp4Extractor {
rootAtomBytesRead = 0;
}
break;
case STATE_READING_CENC_AUXILIARY_DATA:
cencAuxiliaryBytesRead = 0;
break;
case STATE_READING_SAMPLE_START:
sampleBytesRead = 0;
break;
}
parserState = state;
}
private int readAtomHeader(NonBlockingInputStream inputStream) {
int remainingBytes = ATOM_HEADER_SIZE - atomBytesRead;
int bytesRead = inputStream.read(atomHeader.getData(), atomBytesRead, remainingBytes);
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
@ -377,13 +350,10 @@ public final class FragmentedMp4Extractor {
atomType = atomHeader.readInt();
if (atomType == Atom.TYPE_mdat) {
int cencAuxSize = fragmentRun.auxiliarySampleInfoTotalSize;
if (cencAuxSize > 0) {
cencAuxiliaryData = new ParsableByteArray(cencAuxSize);
enterState(STATE_READING_CENC_AUXILIARY_DATA);
if (fragmentRun.sampleEncryptionDataNeedsFill) {
enterState(STATE_READING_ENCRYPTION_DATA);
} else {
cencAuxiliaryData = null;
enterState(STATE_READING_SAMPLE_START);
enterState(STATE_READING_SAMPLE);
}
return 0;
}
@ -395,7 +365,7 @@ public final class FragmentedMp4Extractor {
containerAtomEndPoints.add(rootAtomBytesRead + atomSize - ATOM_HEADER_SIZE);
} else {
atomData = new ParsableByteArray(atomSize);
System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, ATOM_HEADER_SIZE);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, ATOM_HEADER_SIZE);
enterState(STATE_READING_ATOM_PAYLOAD);
}
} else {
@ -409,7 +379,7 @@ public final class FragmentedMp4Extractor {
private int readAtomPayload(NonBlockingInputStream inputStream) {
int bytesRead;
if (atomData != null) {
bytesRead = inputStream.read(atomData.getData(), atomBytesRead, atomSize - atomBytesRead);
bytesRead = inputStream.read(atomData.data, atomBytesRead, atomSize - atomBytesRead);
} else {
bytesRead = inputStream.skip(atomSize - atomBytesRead);
}
@ -441,8 +411,8 @@ public final class FragmentedMp4Extractor {
if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) {
segmentIndex = parseSidx(leaf.getData());
return RESULT_READ_SIDX;
segmentIndex = parseSidx(leaf.data);
return RESULT_READ_INDEX;
}
return 0;
}
@ -450,7 +420,7 @@ public final class FragmentedMp4Extractor {
private int onContainerAtomRead(ContainerAtom container) {
if (container.type == Atom.TYPE_moov) {
onMoovContainerAtomRead(container);
return RESULT_READ_MOOV;
return RESULT_READ_INIT;
} else if (container.type == Atom.TYPE_moof) {
onMoofContainerAtomRead(container);
} else if (!containerAtoms.isEmpty()) {
@ -460,11 +430,12 @@ public final class FragmentedMp4Extractor {
}
private void onMoovContainerAtomRead(ContainerAtom moov) {
List<Atom> moovChildren = moov.getChildren();
for (int i = 0; i < moovChildren.size(); i++) {
List<Atom> moovChildren = moov.children;
int moovChildrenSize = moovChildren.size();
for (int i = 0; i < moovChildrenSize; i++) {
Atom child = moovChildren.get(i);
if (child.type == Atom.TYPE_pssh) {
ParsableByteArray psshAtom = ((LeafAtom) child).getData();
ParsableByteArray psshAtom = ((LeafAtom) child).data;
psshAtom.setPosition(FULL_ATOM_HEADER_SIZE);
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
int dataSize = psshAtom.readInt();
@ -474,13 +445,13 @@ public final class FragmentedMp4Extractor {
}
}
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).getData());
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
track = parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak));
}
private void onMoofContainerAtomRead(ContainerAtom moof) {
fragmentRun = new TrackFragment();
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags);
fragmentRun.reset();
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
sampleIndex = 0;
lastSyncSampleIndex = 0;
pendingSeekSyncSampleIndex = 0;
@ -514,21 +485,21 @@ public final class FragmentedMp4Extractor {
*/
private static Track parseTrak(ContainerAtom trak) {
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).getData());
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO);
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).getData());
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
int id = header.first;
// TODO: This value should be used to set a duration field on the Track object
// instantiated below, however we've found examples where the value is 0. Revisit whether we
// should set it anyway (and just have it be wrong for bad media streams).
// long duration = header.second;
long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).getData());
long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
.getContainerAtomOfType(Atom.TYPE_stbl);
Pair<MediaFormat, TrackEncryptionBox[]> sampleDescriptions =
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).getData());
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data);
return new Track(id, trackType, timescale, sampleDescriptions.first, sampleDescriptions.second);
}
@ -654,7 +625,7 @@ public final class FragmentedMp4Extractor {
if (childAtomType == Atom.TYPE_esds) {
initializationData = parseEsdsFromParent(parent, childStartPosition);
// TODO: Do we really need to do this? See [redacted]
// Update sampleRate and sampleRate from the AudioSpecificConfig initialization data.
// Update sampleRate and channelCount from the AudioSpecificConfig initialization data.
Pair<Integer, Integer> audioSpecificConfig =
CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData);
sampleRate = audioSpecificConfig.first;
@ -697,7 +668,7 @@ public final class FragmentedMp4Extractor {
int length = atom.readUnsignedShort();
int offset = atom.getPosition();
atom.skip(length);
return CodecSpecificDataUtil.buildNalUnit(atom.getData(), offset, length);
return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length);
}
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
@ -775,7 +746,7 @@ public final class FragmentedMp4Extractor {
parent.skip(13);
// Start of AudioSpecificConfig (defined in 14496-3)
parent.skip(1); // AudioSpecificConfig tag
parent.skip(1); // AudioSpecificConfig tag
varIntByte = parent.readUnsignedByte();
int varInt = varIntByte & 0x7F;
while (varIntByte > 127) {
@ -789,49 +760,47 @@ public final class FragmentedMp4Extractor {
}
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom moof, TrackFragment out, int workaroundFlags) {
// TODO: Consider checking that the sequence number returned by parseMfhd is as expected.
parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
out, workaroundFlags);
}
/**
* Parses an mfhd atom (defined in 14496-12).
*
* @param mfhd The mfhd atom to parse.
* @return The sequence number of the fragment.
*/
private static int parseMfhd(ParsableByteArray mfhd) {
mfhd.setPosition(FULL_ATOM_HEADER_SIZE);
return mfhd.readUnsignedIntToInt();
out, workaroundFlags, extendedTypeScratch);
}
/**
* Parses a traf atom (defined in 14496-12).
*/
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom traf, TrackFragment out, int workaroundFlags) {
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
parseSaiz(saiz.getData(), out);
}
ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
long decodeTime = tfdtAtom == null ? 0
: parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).getData());
long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.getData());
out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
TrackEncryptionBox trackEncryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
parseSaiz(trackEncryptionBox, saiz.data, out);
}
LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
if (senc != null) {
parseSenc(senc.data, out);
}
LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
if (uuid != null) {
parseUuid(uuid.getData(), out);
parseUuid(uuid.data, out, extendedTypeScratch);
}
}
private static void parseSaiz(ParsableByteArray saiz, TrackFragment out) {
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
TrackFragment out) {
int vectorSize = encryptionBox.initializationVectorSize;
saiz.setPosition(ATOM_HEADER_SIZE);
int fullAtom = saiz.readInt();
int flags = parseFullAtomFlags(fullAtom);
@ -839,21 +808,26 @@ public final class FragmentedMp4Extractor {
saiz.skip(8);
}
int defaultSampleInfoSize = saiz.readUnsignedByte();
int sampleCount = saiz.readUnsignedIntToInt();
if (sampleCount != out.length) {
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
}
int totalSize = 0;
int[] sampleInfoSizes = new int[sampleCount];
if (defaultSampleInfoSize == 0) {
boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
for (int i = 0; i < sampleCount; i++) {
sampleInfoSizes[i] = saiz.readUnsignedByte();
totalSize += sampleInfoSizes[i];
int sampleInfoSize = saiz.readUnsignedByte();
totalSize += sampleInfoSize;
sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
}
} else {
for (int i = 0; i < sampleCount; i++) {
sampleInfoSizes[i] = defaultSampleInfoSize;
totalSize += defaultSampleInfoSize;
}
boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
totalSize += defaultSampleInfoSize * sampleCount;
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
}
out.setAuxiliarySampleInfoTables(totalSize, sampleInfoSizes);
out.initEncryptionData(totalSize);
}
/**
@ -912,10 +886,9 @@ public final class FragmentedMp4Extractor {
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
trun.setPosition(ATOM_HEADER_SIZE);
int fullAtom = trun.readInt();
int version = parseFullAtomVersion(fullAtom);
int flags = parseFullAtomFlags(fullAtom);
int numberOfEntries = trun.readUnsignedIntToInt();
int sampleCount = trun.readUnsignedIntToInt();
if ((flags & 0x01 /* data_offset_present */) != 0) {
trun.skip(4);
}
@ -932,17 +905,18 @@ public final class FragmentedMp4Extractor {
boolean sampleCompositionTimeOffsetsPresent =
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
int[] sampleSizeTable = new int[numberOfEntries];
int[] sampleDecodingTimeTable = new int[numberOfEntries];
int[] sampleCompositionTimeOffsetTable = new int[numberOfEntries];
boolean[] sampleIsSyncFrameTable = new boolean[numberOfEntries];
out.initTables(sampleCount);
int[] sampleSizeTable = out.sampleSizeTable;
int[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
long timescale = track.timescale;
long cumulativeTime = decodeTime;
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
for (int i = 0; i < numberOfEntries; i++) {
for (int i = 0; i < sampleCount; i++) {
// Use trun values if present, otherwise tfhd, otherwise trex.
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
: defaultSampleValues.duration;
@ -950,48 +924,47 @@ public final class FragmentedMp4Extractor {
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
if (sampleCompositionTimeOffsetsPresent) {
int sampleOffset;
if (version == 0) {
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
// version 0 trun boxes, however a significant number of streams violate the spec and use
// signed integers instead. It's safe to always parse sample offsets as signed integers
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
sampleOffset = trun.readInt();
} else {
sampleOffset = trun.readInt();
}
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
// version 0 trun boxes, however a significant number of streams violate the spec and use
// signed integers instead. It's safe to always parse sample offsets as signed integers
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
int sampleOffset = trun.readInt();
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
} else {
sampleCompositionTimeOffsetTable[i] = 0;
}
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
sampleSizeTable[i] = sampleSize;
boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
if (workaroundEveryVideoFrameIsSyncFrame && i != 0) {
isSync = false;
}
if (isSync) {
sampleIsSyncFrameTable[i] = true;
}
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
cumulativeTime += sampleDuration;
}
out.setSampleTables(sampleSizeTable, sampleDecodingTimeTable, sampleCompositionTimeOffsetTable,
sampleIsSyncFrameTable);
}
private static void parseUuid(ParsableByteArray uuid, TrackFragment out) {
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
byte[] extendedTypeScratch) {
uuid.setPosition(ATOM_HEADER_SIZE);
byte[] extendedType = new byte[16];
uuid.readBytes(extendedType, 0, 16);
uuid.readBytes(extendedTypeScratch, 0, 16);
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
if (!Arrays.equals(extendedType, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
return;
}
// See "Portable encoding of audio-video objects: The Protected Interoperable File Format
// (PIFF), John A. Bocharov et al, Section 5.3.2.1."
int fullAtom = uuid.readInt();
// Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
// audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
// Section 5.3.2.1."
parseSenc(uuid, 16, out);
}
private static void parseSenc(ParsableByteArray senc, TrackFragment out) {
parseSenc(senc, 0, out);
}
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
senc.setPosition(ATOM_HEADER_SIZE + offset);
int fullAtom = senc.readInt();
int flags = parseFullAtomFlags(fullAtom);
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
@ -1000,15 +973,14 @@ public final class FragmentedMp4Extractor {
}
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
int numberOfEntries = uuid.readUnsignedIntToInt();
if (numberOfEntries != out.length) {
throw new IllegalStateException("Length mismatch: " + numberOfEntries + ", " + out.length);
int sampleCount = senc.readUnsignedIntToInt();
if (sampleCount != out.length) {
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
}
int sampleEncryptionDataLength = uuid.length() - uuid.getPosition();
ParsableByteArray sampleEncryptionData = new ParsableByteArray(sampleEncryptionDataLength);
uuid.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionData.length());
out.setSmoothStreamingSampleEncryptionData(sampleEncryptionData, subsampleEncryption);
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
out.initEncryptionData(senc.length() - senc.getPosition());
out.fillEncryptionData(senc);
}
/**
@ -1067,18 +1039,12 @@ public final class FragmentedMp4Extractor {
return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs);
}
private int readCencAuxiliaryData(NonBlockingInputStream inputStream) {
int length = cencAuxiliaryData.length();
int bytesRead = inputStream.read(cencAuxiliaryData.getData(), cencAuxiliaryBytesRead,
length - cencAuxiliaryBytesRead);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
cencAuxiliaryBytesRead += bytesRead;
if (cencAuxiliaryBytesRead < length) {
private int readEncryptionData(NonBlockingInputStream inputStream) {
boolean success = fragmentRun.fillEncryptionData(inputStream);
if (!success) {
return RESULT_NEED_MORE_DATA;
}
enterState(STATE_READING_SAMPLE_START);
enterState(STATE_READING_SAMPLE);
return 0;
}
@ -1105,89 +1071,62 @@ public final class FragmentedMp4Extractor {
enterState(STATE_READING_ATOM_HEADER);
return 0;
}
if (sampleIndex < pendingSeekSyncSampleIndex) {
return skipSample(inputStream);
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
if (inputStream.getAvailableByteCount() < sampleSize) {
return RESULT_NEED_MORE_DATA;
}
return readSample(inputStream, out);
if (sampleIndex < pendingSeekSyncSampleIndex) {
return skipSample(inputStream, sampleSize);
}
return readSample(inputStream, sampleSize, out);
}
private int skipSample(NonBlockingInputStream inputStream) {
if (parserState == STATE_READING_SAMPLE_START) {
ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
: fragmentRun.smoothStreamingSampleEncryptionData;
if (sampleEncryptionData != null) {
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = cencAuxiliaryData != null
? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
: fragmentRun.smoothStreamingUsesSubsampleEncryption;
sampleEncryptionData.skip(vectorSize);
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
if (subsampleEncryption) {
sampleEncryptionData.skip((2 + 4) * subsampleCount);
}
private int skipSample(NonBlockingInputStream inputStream, int sampleSize) {
if (fragmentRun.definesEncryptionData) {
ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData;
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
sampleEncryptionData.skip(vectorSize);
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
if (subsampleEncryption) {
sampleEncryptionData.skip((2 + 4) * subsampleCount);
}
}
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
int bytesRead = inputStream.skip(sampleSize - sampleBytesRead);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
sampleBytesRead += bytesRead;
if (sampleSize != sampleBytesRead) {
enterState(STATE_READING_SAMPLE_INCREMENTAL);
return RESULT_NEED_MORE_DATA;
}
inputStream.skip(sampleSize);
sampleIndex++;
enterState(STATE_READING_SAMPLE_START);
enterState(STATE_READING_SAMPLE);
return 0;
}
@SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) {
if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
}
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
ByteBuffer outputData = out.data;
if (parserState == STATE_READING_SAMPLE_START) {
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
out.flags = 0;
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC;
lastSyncSampleIndex = sampleIndex;
}
if (out.allowDataBufferReplacement
&& (out.data == null || out.data.capacity() < sampleSize)) {
outputData = ByteBuffer.allocate(sampleSize);
out.data = outputData;
}
ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
: fragmentRun.smoothStreamingSampleEncryptionData;
if (sampleEncryptionData != null) {
readSampleEncryptionData(sampleEncryptionData, out);
}
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
out.flags = 0;
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC;
lastSyncSampleIndex = sampleIndex;
}
if (out.allowDataBufferReplacement && (out.data == null || out.data.capacity() < sampleSize)) {
outputData = ByteBuffer.allocate(sampleSize);
out.data = outputData;
}
if (fragmentRun.definesEncryptionData) {
readSampleEncryptionData(fragmentRun.sampleEncryptionData, out);
}
int bytesRead;
if (outputData == null) {
bytesRead = inputStream.skip(sampleSize - sampleBytesRead);
inputStream.skip(sampleSize);
out.size = 0;
} else {
bytesRead = inputStream.read(outputData, sampleSize - sampleBytesRead);
}
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
sampleBytesRead += bytesRead;
if (sampleSize != sampleBytesRead) {
enterState(STATE_READING_SAMPLE_INCREMENTAL);
return RESULT_NEED_MORE_DATA | RESULT_READ_SAMPLE_PARTIAL;
}
if (outputData != null) {
inputStream.read(outputData, sampleSize);
if (track.type == Track.TYPE_VIDEO) {
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code
// delimited content. Replace length prefixes with start codes.
@ -1203,13 +1142,11 @@ public final class FragmentedMp4Extractor {
outputData.position(sampleOffset + sampleSize);
}
out.size = sampleSize;
} else {
out.size = 0;
}
sampleIndex++;
enterState(STATE_READING_SAMPLE_START);
return RESULT_READ_SAMPLE_FULL;
enterState(STATE_READING_SAMPLE);
return RESULT_READ_SAMPLE;
}
@SuppressLint("InlinedApi")
@ -1219,9 +1156,7 @@ public final class FragmentedMp4Extractor {
byte[] keyId = encryptionBox.keyId;
boolean isEncrypted = encryptionBox.isEncrypted;
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = cencAuxiliaryData != null
? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
: fragmentRun.smoothStreamingUsesSubsampleEncryption;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
byte[] vector = out.cryptoInfo.iv;
if (vector == null || vector.length != 16) {

View file

@ -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;
}

View file

@ -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.

View file

@ -15,48 +15,136 @@
*/
package com.google.android.exoplayer.parser.mp4;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
/**
* A holder for information corresponding to a single fragment of an mp4 file.
*/
/* package */ class TrackFragment {
/* package */ final class TrackFragment {
public int sampleDescriptionIndex;
/**
* The number of samples contained by the fragment.
*/
public int length;
/**
* The size of each sample in the run.
*/
public int[] sampleSizeTable;
/**
* The decoding time of each sample in the run.
*/
public int[] sampleDecodingTimeTable;
/**
* The composition time offset of each sample in the run.
*/
public int[] sampleCompositionTimeOffsetTable;
/**
* Indicates which samples are sync frames.
*/
public boolean[] sampleIsSyncFrameTable;
/**
* True if the fragment defines encryption data. False otherwise.
*/
public boolean definesEncryptionData;
/**
* If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.
* Undefined otherwise.
*/
public boolean[] sampleHasSubsampleEncryptionTable;
/**
* If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.
* Undefined otherwise.
*/
public int sampleEncryptionDataLength;
/**
* If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined
* otherwise.
*/
public ParsableByteArray sampleEncryptionData;
/**
* Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.
*/
public boolean sampleEncryptionDataNeedsFill;
public int auxiliarySampleInfoTotalSize;
public int[] auxiliarySampleInfoSizeTable;
public boolean smoothStreamingUsesSubsampleEncryption;
public ParsableByteArray smoothStreamingSampleEncryptionData;
public void setSampleDescriptionIndex(int sampleDescriptionIndex) {
this.sampleDescriptionIndex = sampleDescriptionIndex;
/**
* Resets the fragment.
* <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 setSampleTables(int[] sampleSizeTable, int[] sampleDecodingTimeTable,
int[] sampleCompositionTimeOffsetTable, boolean[] sampleIsSyncFrameTable) {
this.sampleSizeTable = sampleSizeTable;
this.sampleDecodingTimeTable = sampleDecodingTimeTable;
this.sampleCompositionTimeOffsetTable = sampleCompositionTimeOffsetTable;
this.sampleIsSyncFrameTable = sampleIsSyncFrameTable;
this.length = sampleSizeTable.length;
/**
* Configures the fragment for the specified number of samples.
* <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 setAuxiliarySampleInfoTables(int totalAuxiliarySampleInfoSize,
int[] auxiliarySampleInfoSizeTable) {
this.auxiliarySampleInfoTotalSize = totalAuxiliarySampleInfoSize;
this.auxiliarySampleInfoSizeTable = auxiliarySampleInfoSizeTable;
/**
* Configures the fragment to be one that defines encryption data of the specified length.
* <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 setSmoothStreamingSampleEncryptionData(ParsableByteArray data,
boolean usesSubsampleEncryption) {
this.smoothStreamingSampleEncryptionData = data;
this.smoothStreamingUsesSubsampleEncryption = usesSubsampleEncryption;
/**
* Fills {@link #sampleEncryptionData} from the provided source.
*
* @param source A source from which to read the encryption data.
*/
public void fillEncryptionData(ParsableByteArray source) {
source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
sampleEncryptionData.setPosition(0);
sampleEncryptionDataNeedsFill = false;
}
/**
* Fills {@link #sampleEncryptionData} for the current run from the provided source.
*
* @param source A source from which to read the encryption data.
* @return True if the encryption data was filled. False if the source had insufficient data.
*/
public boolean fillEncryptionData(NonBlockingInputStream source) {
if (source.getAvailableByteCount() < sampleEncryptionDataLength) {
return false;
}
source.read(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
sampleEncryptionData.setPosition(0);
sampleEncryptionDataNeedsFill = false;
return true;
}
public int getSamplePresentationTime(int index) {

View file

@ -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;

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@ -77,13 +78,15 @@ public final class DefaultWebmExtractor implements WebmExtractor {
private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder tempSampleHolder;
private boolean sampleRead;
private SampleHolder sampleHolder;
private int readResults;
private boolean prepared = false;
private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L;
@ -105,28 +108,29 @@ public final class DefaultWebmExtractor implements WebmExtractor {
/* package */ DefaultWebmExtractor(EbmlReader reader) {
this.reader = reader;
this.reader.setEventHandler(new InnerEbmlEventHandler());
this.cueTimesUs = new LongArray();
this.cueClusterPositions = new LongArray();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
tempSampleHolder = sampleHolder;
sampleRead = false;
reader.read(inputStream);
tempSampleHolder = null;
return sampleRead;
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
this.sampleHolder = sampleHolder;
this.readResults = 0;
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
int ebmlReadResult = reader.read(inputStream);
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
}
}
this.sampleHolder = null;
return readResults;
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
checkPrepared();
if (allowNoop
&& cues != null
&& clusterTimecodeUs != UNKNOWN
&& simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
@ -134,19 +138,19 @@ public final class DefaultWebmExtractor implements WebmExtractor {
return false;
}
}
clusterTimecodeUs = UNKNOWN;
simpleBlockTimecodeUs = UNKNOWN;
reader.reset();
return true;
}
@Override
public SegmentIndex getCues() {
checkPrepared();
public SegmentIndex getIndex() {
return cues;
}
@Override
public MediaFormat getFormat() {
checkPrepared();
return format;
}
@ -196,6 +200,8 @@ public final class DefaultWebmExtractor implements WebmExtractor {
break;
case ID_CUES:
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
break;
default:
// pass
@ -204,11 +210,16 @@ public final class DefaultWebmExtractor implements WebmExtractor {
}
/* package */ boolean onMasterElementEnd(int id) {
if (id == ID_CUES) {
finishPreparing();
return false;
switch (id) {
case ID_CUES:
buildCues();
return false;
case ID_VIDEO:
buildFormat();
return true;
default:
return true;
}
return true;
}
/* package */ boolean onIntegerElement(int id, long value) {
@ -283,6 +294,12 @@ public final class DefaultWebmExtractor implements WebmExtractor {
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// for info about how data is organized in a SimpleBlock element.
// If we don't have a sample holder then don't consume the data.
if (sampleHolder == null) {
readResults |= RESULT_NEED_SAMPLE_HOLDER;
return false;
}
// Value of trackNumber is not used but needs to be read.
reader.readVarint(inputStream);
@ -304,10 +321,10 @@ public final class DefaultWebmExtractor implements WebmExtractor {
case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
tempSampleHolder.decodeOnly = invisible;
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
sampleHolder.decodeOnly = invisible;
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
@ -316,44 +333,63 @@ public final class DefaultWebmExtractor implements WebmExtractor {
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
// Read video data into sample holder.
reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
sampleRead = true;
return false;
} else {
reader.skipBytes(inputStream, contentsSizeBytes);
return true;
ByteBuffer outputData = sampleHolder.data;
if (sampleHolder.allowDataBufferReplacement
&& (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
outputData = ByteBuffer.allocate(sampleHolder.size);
sampleHolder.data = outputData;
}
if (outputData == null) {
reader.skipBytes(inputStream, sampleHolder.size);
sampleHolder.size = 0;
} else {
reader.readBytes(inputStream, outputData, sampleHolder.size);
}
readResults |= RESULT_READ_SAMPLE;
}
return true;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
private void checkPrepared() {
if (!prepared) {
throw new IllegalStateException("Parser not yet prepared");
/**
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
*
* <p>Replaces the previous {@link #format} only if video width/height have changed.
* {@link #format} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
private void buildFormat() {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT;
} else if (format == null) {
throw new IllegalStateException("Unable to build format");
}
}
private void finishPreparing() {
if (prepared) {
throw new IllegalStateException("Already prepared");
} else if (segmentStartOffsetBytes == UNKNOWN) {
/**
* Build a {@link SegmentIndex} containing recently gathered Cues information.
*
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
private void buildCues() {
if (segmentStartOffsetBytes == UNKNOWN) {
throw new IllegalStateException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) {
throw new IllegalStateException("Duration unknown");
} else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
throw new IllegalStateException("Pixel width/height unknown");
} else if (cuesSizeBytes == UNKNOWN) {
throw new IllegalStateException("Cues size unknown");
} else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
} else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Invalid/missing cue points");
}
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize];
long[] offsets = new long[cuePointsSize];
@ -372,8 +408,7 @@ public final class DefaultWebmExtractor implements WebmExtractor {
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
prepared = true;
readResults |= RESULT_READ_INDEX;
}
/**
@ -388,30 +423,30 @@ public final class DefaultWebmExtractor implements WebmExtractor {
}
@Override
public boolean onMasterElementStart(
public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
return DefaultWebmExtractor.this.onMasterElementStart(
DefaultWebmExtractor.this.onMasterElementStart(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
}
@Override
public boolean onMasterElementEnd(int id) {
return DefaultWebmExtractor.this.onMasterElementEnd(id);
public void onMasterElementEnd(int id) {
DefaultWebmExtractor.this.onMasterElementEnd(id);
}
@Override
public boolean onIntegerElement(int id, long value) {
return DefaultWebmExtractor.this.onIntegerElement(id, value);
public void onIntegerElement(int id, long value) {
DefaultWebmExtractor.this.onIntegerElement(id, value);
}
@Override
public boolean onFloatElement(int id, double value) {
return DefaultWebmExtractor.this.onFloatElement(id, value);
public void onFloatElement(int id, double value) {
DefaultWebmExtractor.this.onFloatElement(id, value);
}
@Override
public boolean onStringElement(int id, String value) {
return DefaultWebmExtractor.this.onStringElement(id, value);
public void onStringElement(int id, String value) {
DefaultWebmExtractor.this.onStringElement(id, value);
}
@Override

View file

@ -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,

View file

@ -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

View file

@ -30,24 +30,38 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
public interface WebmExtractor {
/**
* Whether the has parsed the cues and sample format from the stream.
*
* @return True if the extractor is prepared. False otherwise
* An attempt to read from the input stream returned insufficient data.
*/
public boolean isPrepared();
public static final int RESULT_NEED_MORE_DATA = 1;
/**
* The end of the input stream was reached.
*/
public static final int RESULT_END_OF_STREAM = 2;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()}.
*/
public static final int RESULT_READ_INIT = 8;
/**
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
/**
* Consumes data from a {@link NonBlockingInputStream}.
*
* <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.

View file

@ -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) {

View file

@ -24,6 +24,28 @@ package com.google.android.exoplayer.upstream;
*/
public interface Allocation {
/**
* Ensures the allocation has a capacity greater than or equal to the specified size in bytes.
* <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.
*

View file

@ -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();
}

View file

@ -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;
}
/**
@ -112,6 +136,16 @@ public final class BufferPool implements Allocator {
this.buffers = buffers;
}
@Override
public void ensureCapacity(int size) {
buffers = allocate(size, buffers);
}
@Override
public int capacity() {
return bufferLength * buffers.length;
}
@Override
public byte[][] getBuffers() {
return buffers;

View file

@ -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);

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import android.net.Uri;
@ -24,13 +25,6 @@ import android.net.Uri;
*/
public final class DataSpec {
/**
* A permitted value of {@link #length}. A {@link DataSpec} defined with this length represents
* the region of media data that starts at its {@link #position} and extends to the end of the
* data whose location is defined by its {@link #uri}.
*/
public static final int LENGTH_UNBOUNDED = -1;
/**
* Identifies the source from which data should be read.
*/
@ -50,7 +44,7 @@ public final class DataSpec {
*/
public final long position;
/**
* The length of the data. Greater than zero, or equal to {@link #LENGTH_UNBOUNDED}.
* The length of the data. Greater than zero, or equal to {@link C#LENGTH_UNBOUNDED}.
*/
public final long length;
/**
@ -98,7 +92,7 @@ public final class DataSpec {
boolean uriIsFullStream) {
Assertions.checkArgument(absoluteStreamPosition >= 0);
Assertions.checkArgument(position >= 0);
Assertions.checkArgument(length > 0 || length == LENGTH_UNBOUNDED);
Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED);
Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
this.uri = uri;
this.uriIsFullStream = uriIsFullStream;

View file

@ -38,11 +38,11 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
*
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
* @param bytes The number of bytes transferred.
* @param bandwidthEstimate The estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no
* estimate is available. Note that this estimate is typically derived from more information
* than {@code bytes} and {@code elapsedMs}.
* @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
* is available. Note that this estimate is typically derived from more information than
* {@code bytes} and {@code elapsedMs}.
*/
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
}
@ -53,9 +53,9 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
private final Clock clock;
private final SlidingPercentile slidingPercentile;
private long accumulator;
private long bytesAccumulator;
private long startTimeMs;
private long bandwidthEstimate;
private long bitrateEstimate;
private int streamCount;
public DefaultBandwidthMeter() {
@ -80,17 +80,12 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
this.eventListener = eventListener;
this.clock = clock;
this.slidingPercentile = new SlidingPercentile(maxWeight);
bandwidthEstimate = NO_ESTIMATE;
bitrateEstimate = NO_ESTIMATE;
}
/**
* Gets the estimated bandwidth.
*
* @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available.
*/
@Override
public synchronized long getEstimate() {
return bandwidthEstimate;
public synchronized long getBitrateEstimate() {
return bitrateEstimate;
}
@Override
@ -103,7 +98,7 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
@Override
public synchronized void onBytesTransferred(int bytes) {
accumulator += bytes;
bytesAccumulator += bytes;
}
@Override
@ -112,32 +107,26 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
long nowMs = clock.elapsedRealtime();
int elapsedMs = (int) (nowMs - startTimeMs);
if (elapsedMs > 0) {
float bytesPerSecond = accumulator * 1000 / elapsedMs;
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs;
slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond);
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
: (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate);
}
streamCount--;
if (streamCount > 0) {
startTimeMs = nowMs;
}
accumulator = 0;
bytesAccumulator = 0;
}
// TODO: Use media time (bytes / mediaRate) as weight.
private int computeWeight(long mediaBytes) {
return (int) Math.sqrt(mediaBytes);
}
private void notifyBandwidthSample(final int elapsedMs, final long bytes,
final long bandwidthEstimate) {
private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate);
eventListener.onBandwidthSample(elapsedMs, bytes, bitrate);
}
});
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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);

View file

@ -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;

View file

@ -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;
}
}
}