mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
79c2f535c6
57 changed files with 1087 additions and 734 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal 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
|
||||||
20
README.md
20
README.md
|
|
@ -55,6 +55,22 @@ accompanying demo application. To get started:
|
||||||
|
|
||||||
## Using Gradle ##
|
## Using Gradle ##
|
||||||
|
|
||||||
ExoPlayer can also be built using Gradle. For a complete list of tasks, run:
|
ExoPlayer can also be built using Gradle. You can include it as a dependent project and build from source. e.g.
|
||||||
|
|
||||||
./gradlew tasks
|
```
|
||||||
|
// setting.gradle
|
||||||
|
include ':app', ':..:ExoPlayer:library'
|
||||||
|
|
||||||
|
// app/build.gradle
|
||||||
|
dependencies {
|
||||||
|
compile project(':..:ExoPlayer:library')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use ExoPlayer as a jar, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew jarRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
and copy library.jar to the libs-folder of your new project.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:0.10.+'
|
classpath 'com.android.tools.build:gradle:0.12.+'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.google.android.exoplayer.demo"
|
package="com.google.android.exoplayer.demo"
|
||||||
android:versionCode="1010"
|
android:versionCode="1012"
|
||||||
android:versionName="1.0.10"
|
android:versionName="1.0.12"
|
||||||
android:theme="@style/RootTheme">
|
android:theme="@style/RootTheme">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,9 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
|
||||||
// DemoPlayer.InfoListener
|
// DemoPlayer.InfoListener
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) {
|
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||||
Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes +
|
Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes +
|
||||||
", " + getTimeString(elapsedMs) + ", " + bandwidthEstimate + "]");
|
", " + getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -92,7 +92,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
|
int mediaStartTimeMs, int mediaEndTimeMs, long length) {
|
||||||
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
||||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||||
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId
|
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId
|
||||||
|
|
@ -101,7 +101,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(int sourceId) {
|
public void onLoadCompleted(int sourceId, long bytesLoaded) {
|
||||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||||
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
|
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
|
||||||
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " +
|
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " +
|
||||||
|
|
|
||||||
|
|
@ -98,12 +98,12 @@ import android.widget.TextView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getDurationUs() {
|
protected long getDurationUs() {
|
||||||
return TrackRenderer.MATCH_LONGEST;
|
return TrackRenderer.MATCH_LONGEST_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getBufferedPositionUs() {
|
protected long getBufferedPositionUs() {
|
||||||
return TrackRenderer.END_OF_TRACK;
|
return TrackRenderer.END_OF_TRACK_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
||||||
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
|
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
|
||||||
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
|
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
|
||||||
void onDroppedFrames(int count, long elapsed);
|
void onDroppedFrames(int count, long elapsed);
|
||||||
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
|
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
|
||||||
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
int mediaStartTimeMs, int mediaEndTimeMs, long length);
|
||||||
void onLoadCompleted(int sourceId);
|
void onLoadCompleted(int sourceId, long bytesLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -391,9 +391,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) {
|
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||||
if (infoListener != null) {
|
if (infoListener != null) {
|
||||||
infoListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate);
|
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,34 +471,34 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
|
int mediaStartTimeMs, int mediaEndTimeMs, long length) {
|
||||||
if (infoListener != null) {
|
if (infoListener != null) {
|
||||||
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
|
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
|
||||||
mediaEndTimeMs, totalBytes);
|
mediaEndTimeMs, length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(int sourceId) {
|
public void onLoadCompleted(int sourceId, long bytesLoaded) {
|
||||||
if (infoListener != null) {
|
if (infoListener != null) {
|
||||||
infoListener.onLoadCompleted(sourceId);
|
infoListener.onLoadCompleted(sourceId, bytesLoaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCanceled(int sourceId) {
|
public void onLoadCanceled(int sourceId, long bytesLoaded) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
||||||
long totalBytes) {
|
long bytesDiscarded) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
||||||
long totalBytes) {
|
long bytesDiscarded) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,14 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android.libraryVariants.all { variant ->
|
||||||
|
def name = variant.buildType.name
|
||||||
|
if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
|
||||||
|
return; // Skip debug builds.
|
||||||
|
}
|
||||||
|
def task = project.tasks.create "jar${name.capitalize()}", Jar
|
||||||
|
task.dependsOn variant.javaCompile
|
||||||
|
task.from variant.javaCompile.destinationDir
|
||||||
|
artifacts.add('archives', task);
|
||||||
|
}
|
||||||
|
|
|
||||||
30
library/src/main/java/com/google/android/exoplayer/C.java
Normal file
30
library/src/main/java/com/google/android/exoplayer/C.java
Normal 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() {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -17,54 +17,41 @@ package com.google.android.exoplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintains codec event counts, for debugging purposes only.
|
* Maintains codec event counts, for debugging purposes only.
|
||||||
|
* <p>
|
||||||
|
* Counters should be written from the playback thread only. Counters may be read from any thread.
|
||||||
|
* To ensure that the counter values are correctly reflected between threads, users of this class
|
||||||
|
* should invoke {@link #ensureUpdated()} prior to reading and after writing.
|
||||||
*/
|
*/
|
||||||
public final class CodecCounters {
|
public final class CodecCounters {
|
||||||
|
|
||||||
public volatile long codecInitCount;
|
public int codecInitCount;
|
||||||
public volatile long codecReleaseCount;
|
public int codecReleaseCount;
|
||||||
public volatile long outputFormatChangedCount;
|
public int outputFormatChangedCount;
|
||||||
public volatile long outputBuffersChangedCount;
|
public int outputBuffersChangedCount;
|
||||||
public volatile long queuedInputBufferCount;
|
public int renderedOutputBufferCount;
|
||||||
public volatile long inputBufferWaitingForSampleCount;
|
public int skippedOutputBufferCount;
|
||||||
public volatile long keyframeCount;
|
public int droppedOutputBufferCount;
|
||||||
public volatile long queuedEndOfStreamCount;
|
|
||||||
public volatile long renderedOutputBufferCount;
|
|
||||||
public volatile long skippedOutputBufferCount;
|
|
||||||
public volatile long droppedOutputBufferCount;
|
|
||||||
public volatile long discardedSamplesCount;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all counts to zero.
|
* Should be invoked from the playback thread after the counters have been updated. Should also
|
||||||
|
* be invoked from any other thread that wishes to read the counters, before reading. These calls
|
||||||
|
* ensure that counter updates are made visible to the reading threads.
|
||||||
*/
|
*/
|
||||||
public void zeroAllCounts() {
|
public synchronized void ensureUpdated() {
|
||||||
codecInitCount = 0;
|
// Do nothing. The use of synchronized ensures a memory barrier should another thread also
|
||||||
codecReleaseCount = 0;
|
// call this method.
|
||||||
outputFormatChangedCount = 0;
|
|
||||||
outputBuffersChangedCount = 0;
|
|
||||||
queuedInputBufferCount = 0;
|
|
||||||
inputBufferWaitingForSampleCount = 0;
|
|
||||||
keyframeCount = 0;
|
|
||||||
queuedEndOfStreamCount = 0;
|
|
||||||
renderedOutputBufferCount = 0;
|
|
||||||
skippedOutputBufferCount = 0;
|
|
||||||
droppedOutputBufferCount = 0;
|
|
||||||
discardedSamplesCount = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDebugString() {
|
public String getDebugString() {
|
||||||
|
ensureUpdated();
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append("cic(").append(codecInitCount).append(")");
|
builder.append("cic(").append(codecInitCount).append(")");
|
||||||
builder.append("crc(").append(codecReleaseCount).append(")");
|
builder.append("crc(").append(codecReleaseCount).append(")");
|
||||||
builder.append("ofc(").append(outputFormatChangedCount).append(")");
|
builder.append("ofc(").append(outputFormatChangedCount).append(")");
|
||||||
builder.append("obc(").append(outputBuffersChangedCount).append(")");
|
builder.append("obc(").append(outputBuffersChangedCount).append(")");
|
||||||
builder.append("qib(").append(queuedInputBufferCount).append(")");
|
|
||||||
builder.append("wib(").append(inputBufferWaitingForSampleCount).append(")");
|
|
||||||
builder.append("kfc(").append(keyframeCount).append(")");
|
|
||||||
builder.append("qes(").append(queuedEndOfStreamCount).append(")");
|
|
||||||
builder.append("ren(").append(renderedOutputBufferCount).append(")");
|
builder.append("ren(").append(renderedOutputBufferCount).append(")");
|
||||||
builder.append("sob(").append(skippedOutputBufferCount).append(")");
|
builder.append("sob(").append(skippedOutputBufferCount).append(")");
|
||||||
builder.append("dob(").append(droppedOutputBufferCount).append(")");
|
builder.append("dob(").append(droppedOutputBufferCount).append(")");
|
||||||
builder.append("dsc(").append(discardedSamplesCount).append(")");
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,14 +316,16 @@ public interface ExoPlayer {
|
||||||
public void seekTo(int positionMs);
|
public void seekTo(int positionMs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops playback.
|
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
|
||||||
|
* is to pause playback.
|
||||||
* <p>
|
* <p>
|
||||||
* Calling this method will cause the playback state to transition to
|
* Calling this method will cause the playback state to transition to
|
||||||
* {@link ExoPlayer#STATE_IDLE}. Note that the player instance can still be used, and that
|
* {@link ExoPlayer#STATE_IDLE}. The player instance can still be used, and
|
||||||
* {@link ExoPlayer#release()} must still be called on the player should it no longer be required.
|
* {@link ExoPlayer#release()} must still be called on the player if it's no longer required.
|
||||||
* <p>
|
* <p>
|
||||||
* Use {@code setPlayWhenReady(false)} rather than this method if the intention is to pause
|
* Calling this method does not reset the playback position. If this player instance will be used
|
||||||
* playback.
|
* to play another video from its start, then {@code seekTo(0)} should be called after stopping
|
||||||
|
* the player and before preparing it for the next video.
|
||||||
*/
|
*/
|
||||||
public void stop();
|
public void stop();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ import java.util.List;
|
||||||
private static final int IDLE_INTERVAL_MS = 1000;
|
private static final int IDLE_INTERVAL_MS = 1000;
|
||||||
|
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
private final HandlerThread internalPlayerThread;
|
private final HandlerThread internalPlaybackThread;
|
||||||
private final Handler eventHandler;
|
private final Handler eventHandler;
|
||||||
private final MediaClock mediaClock;
|
private final MediaClock mediaClock;
|
||||||
private final boolean[] rendererEnabledFlags;
|
private final boolean[] rendererEnabledFlags;
|
||||||
|
|
@ -95,12 +95,12 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = ExoPlayer.STATE_IDLE;
|
this.state = ExoPlayer.STATE_IDLE;
|
||||||
this.durationUs = TrackRenderer.UNKNOWN_TIME;
|
this.durationUs = TrackRenderer.UNKNOWN_TIME_US;
|
||||||
this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME;
|
this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
|
||||||
|
|
||||||
mediaClock = new MediaClock();
|
mediaClock = new MediaClock();
|
||||||
enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
|
enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
|
||||||
internalPlayerThread = new HandlerThread(getClass().getSimpleName() + ":Handler") {
|
internalPlaybackThread = new HandlerThread(getClass().getSimpleName() + ":Handler") {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
|
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
|
||||||
|
|
@ -109,12 +109,12 @@ import java.util.List;
|
||||||
super.run();
|
super.run();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
internalPlayerThread.start();
|
internalPlaybackThread.start();
|
||||||
handler = new Handler(internalPlayerThread.getLooper(), this);
|
handler = new Handler(internalPlaybackThread.getLooper(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Looper getPlaybackLooper() {
|
public Looper getPlaybackLooper() {
|
||||||
return internalPlayerThread.getLooper();
|
return internalPlaybackThread.getLooper();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCurrentPosition() {
|
public int getCurrentPosition() {
|
||||||
|
|
@ -122,12 +122,12 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getBufferedPosition() {
|
public int getBufferedPosition() {
|
||||||
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME
|
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
|
||||||
: (int) (bufferedPositionUs / 1000);
|
: (int) (bufferedPositionUs / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDuration() {
|
public int getDuration() {
|
||||||
return durationUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME
|
return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
|
||||||
: (int) (durationUs / 1000);
|
: (int) (durationUs / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +179,7 @@ import java.util.List;
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internalPlayerThread.quit();
|
internalPlaybackThread.quit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,14 +287,14 @@ import java.util.List;
|
||||||
enabledRenderers.add(renderer);
|
enabledRenderers.add(renderer);
|
||||||
isEnded = isEnded && renderer.isEnded();
|
isEnded = isEnded && renderer.isEnded();
|
||||||
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
||||||
if (durationUs == TrackRenderer.UNKNOWN_TIME) {
|
if (durationUs == TrackRenderer.UNKNOWN_TIME_US) {
|
||||||
// We've already encountered a track for which the duration is unknown, so the media
|
// We've already encountered a track for which the duration is unknown, so the media
|
||||||
// duration is unknown regardless of the duration of this track.
|
// duration is unknown regardless of the duration of this track.
|
||||||
} else {
|
} else {
|
||||||
long trackDurationUs = renderer.getDurationUs();
|
long trackDurationUs = renderer.getDurationUs();
|
||||||
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME) {
|
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) {
|
||||||
durationUs = TrackRenderer.UNKNOWN_TIME;
|
durationUs = TrackRenderer.UNKNOWN_TIME_US;
|
||||||
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST) {
|
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else {
|
} else {
|
||||||
durationUs = Math.max(durationUs, trackDurationUs);
|
durationUs = Math.max(durationUs, trackDurationUs);
|
||||||
|
|
@ -331,11 +331,11 @@ import java.util.List;
|
||||||
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
|
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
|
||||||
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
|
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
|
||||||
return minBufferDurationUs <= 0
|
return minBufferDurationUs <= 0
|
||||||
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME
|
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US
|
||||||
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK
|
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|
||||||
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs
|
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs
|
||||||
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME
|
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
|
||||||
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST
|
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
|
||||||
&& rendererBufferedPositionUs >= rendererDurationUs);
|
&& rendererBufferedPositionUs >= rendererDurationUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,7 +384,7 @@ import java.util.List;
|
||||||
private void doSomeWork() throws ExoPlaybackException {
|
private void doSomeWork() throws ExoPlaybackException {
|
||||||
TraceUtil.beginSection("doSomeWork");
|
TraceUtil.beginSection("doSomeWork");
|
||||||
long operationStartTimeMs = SystemClock.elapsedRealtime();
|
long operationStartTimeMs = SystemClock.elapsedRealtime();
|
||||||
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME ? durationUs
|
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs
|
||||||
: Long.MAX_VALUE;
|
: Long.MAX_VALUE;
|
||||||
boolean isEnded = true;
|
boolean isEnded = true;
|
||||||
boolean allRenderersReadyOrEnded = true;
|
boolean allRenderersReadyOrEnded = true;
|
||||||
|
|
@ -398,17 +398,17 @@ import java.util.List;
|
||||||
isEnded = isEnded && renderer.isEnded();
|
isEnded = isEnded && renderer.isEnded();
|
||||||
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
||||||
|
|
||||||
if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME) {
|
if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
|
||||||
// We've already encountered a track for which the buffered position is unknown. Hence the
|
// We've already encountered a track for which the buffered position is unknown. Hence the
|
||||||
// media buffer position unknown regardless of the buffered position of this track.
|
// media buffer position unknown regardless of the buffered position of this track.
|
||||||
} else {
|
} else {
|
||||||
long rendererDurationUs = renderer.getDurationUs();
|
long rendererDurationUs = renderer.getDurationUs();
|
||||||
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
|
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
|
||||||
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME) {
|
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
|
||||||
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME;
|
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
|
||||||
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK
|
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|
||||||
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME
|
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
|
||||||
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST
|
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
|
||||||
&& rendererBufferedPositionUs >= rendererDurationUs)) {
|
&& rendererBufferedPositionUs >= rendererDurationUs)) {
|
||||||
// This track is fully buffered.
|
// This track is fully buffered.
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -525,7 +525,7 @@ import java.util.List;
|
||||||
notifyAll();
|
notifyAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state != ExoPlayer.STATE_IDLE) {
|
if (state != ExoPlayer.STATE_IDLE && state != ExoPlayer.STATE_PREPARING) {
|
||||||
// The message may have caused something to change that now requires us to do work.
|
// The message may have caused something to change that now requires us to do work.
|
||||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
|
||||||
/**
|
/**
|
||||||
* The version of the library, expressed as a string.
|
* The version of the library, expressed as a string.
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "1.0.11";
|
public static final String VERSION = "1.0.12";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library, expressed as an integer.
|
* The version of the library, expressed as an integer.
|
||||||
|
|
@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo {
|
||||||
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
|
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
|
||||||
* corresponding integer version 1002003.
|
* corresponding integer version 1002003.
|
||||||
*/
|
*/
|
||||||
public static final int VERSION_INT = 1000010;
|
public static final int VERSION_INT = 1000012;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
|
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,12 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||||
extractor = new MediaExtractor();
|
extractor = new MediaExtractor();
|
||||||
extractor.setDataSource(context, uri, headers);
|
extractor.setDataSource(context, uri, headers);
|
||||||
trackStates = new int[extractor.getTrackCount()];
|
trackStates = new int[extractor.getTrackCount()];
|
||||||
pendingDiscontinuities = new boolean[extractor.getTrackCount()];
|
pendingDiscontinuities = new boolean[trackStates.length];
|
||||||
trackInfos = new TrackInfo[trackStates.length];
|
trackInfos = new TrackInfo[trackStates.length];
|
||||||
for (int i = 0; i < trackStates.length; i++) {
|
for (int i = 0; i < trackStates.length; i++) {
|
||||||
android.media.MediaFormat format = extractor.getTrackFormat(i);
|
android.media.MediaFormat format = extractor.getTrackFormat(i);
|
||||||
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
|
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
|
||||||
format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME;
|
format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME_US;
|
||||||
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
|
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
|
||||||
trackInfos[i] = new TrackInfo(mime, duration);
|
trackInfos[i] = new TrackInfo(mime, duration);
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +84,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||||
@Override
|
@Override
|
||||||
public int getTrackCount() {
|
public int getTrackCount() {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
return extractor.getTrackCount();
|
return trackStates.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -97,17 +97,18 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||||
public void enable(int track, long timeUs) {
|
public void enable(int track, long timeUs) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
|
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
|
||||||
boolean wasSourceEnabled = isEnabled();
|
|
||||||
trackStates[track] = TRACK_STATE_ENABLED;
|
trackStates[track] = TRACK_STATE_ENABLED;
|
||||||
extractor.selectTrack(track);
|
extractor.selectTrack(track);
|
||||||
if (!wasSourceEnabled) {
|
seekToUs(timeUs);
|
||||||
seekToUs(timeUs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void continueBuffering(long playbackPositionUs) {
|
public boolean continueBuffering(long playbackPositionUs) {
|
||||||
// Do nothing. The MediaExtractor instance is responsible for buffering.
|
// MediaExtractor takes care of buffering and blocks until it has samples, so we can always
|
||||||
|
// return true here. Although note that the blocking behavior is itself as bug, as per the
|
||||||
|
// TODO further up this file. This method will need to return something else as part of fixing
|
||||||
|
// the TODO.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -122,15 +123,15 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||||
if (onlyReadDiscontinuity) {
|
if (onlyReadDiscontinuity) {
|
||||||
return NOTHING_READ;
|
return NOTHING_READ;
|
||||||
}
|
}
|
||||||
|
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
|
||||||
|
formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16(
|
||||||
|
extractor.getTrackFormat(track));
|
||||||
|
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
|
||||||
|
trackStates[track] = TRACK_STATE_FORMAT_SENT;
|
||||||
|
return FORMAT_READ;
|
||||||
|
}
|
||||||
int extractorTrackIndex = extractor.getSampleTrackIndex();
|
int extractorTrackIndex = extractor.getSampleTrackIndex();
|
||||||
if (extractorTrackIndex == track) {
|
if (extractorTrackIndex == track) {
|
||||||
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
|
|
||||||
formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16(
|
|
||||||
extractor.getTrackFormat(track));
|
|
||||||
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
|
|
||||||
trackStates[track] = TRACK_STATE_FORMAT_SENT;
|
|
||||||
return FORMAT_READ;
|
|
||||||
}
|
|
||||||
if (sampleHolder.data != null) {
|
if (sampleHolder.data != null) {
|
||||||
int offset = sampleHolder.data.position();
|
int offset = sampleHolder.data.position();
|
||||||
sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
|
sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
|
||||||
|
|
@ -187,7 +188,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
long bufferedDurationUs = extractor.getCachedDuration();
|
long bufferedDurationUs = extractor.getCachedDuration();
|
||||||
if (bufferedDurationUs == -1) {
|
if (bufferedDurationUs == -1) {
|
||||||
return TrackRenderer.UNKNOWN_TIME;
|
return TrackRenderer.UNKNOWN_TIME_US;
|
||||||
} else {
|
} else {
|
||||||
return extractor.getSampleTime() + bufferedDurationUs;
|
return extractor.getSampleTime() + bufferedDurationUs;
|
||||||
}
|
}
|
||||||
|
|
@ -202,13 +203,4 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEnabled() {
|
|
||||||
for (int i = 0; i < trackStates.length; i++) {
|
|
||||||
if (trackStates[i] != TRACK_STATE_DISABLED) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,8 +266,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onOutputFormatChanged(MediaFormat format) {
|
protected void onOutputFormatChanged(MediaFormat format) {
|
||||||
releaseAudioTrack();
|
|
||||||
this.sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
|
||||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
switch (channelCount) {
|
switch (channelCount) {
|
||||||
|
|
@ -283,6 +281,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
|
throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||||
|
if (audioTrack != null && this.sampleRate == sampleRate
|
||||||
|
&& this.channelConfig == channelConfig) {
|
||||||
|
// We already have an existing audio track with the correct sample rate and channel config.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseAudioTrack();
|
||||||
|
this.sampleRate = sampleRate;
|
||||||
this.channelConfig = channelConfig;
|
this.channelConfig = channelConfig;
|
||||||
this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig,
|
this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT);
|
AudioFormat.ENCODING_PCM_16BIT);
|
||||||
|
|
@ -417,7 +425,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isReady() {
|
protected boolean isReady() {
|
||||||
return getPendingFrameCount() > 0;
|
return super.isReady() || getPendingFrameCount() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
private int codecReconfigurationState;
|
private int codecReconfigurationState;
|
||||||
|
|
||||||
private int trackIndex;
|
private int trackIndex;
|
||||||
|
private boolean sourceIsReady;
|
||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
private boolean outputStreamEnded;
|
private boolean outputStreamEnded;
|
||||||
private boolean waitingForKeys;
|
private boolean waitingForKeys;
|
||||||
|
|
@ -186,7 +187,12 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
return TrackRenderer.STATE_IGNORE;
|
return TrackRenderer.STATE_IGNORE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
/**
|
||||||
|
* Determines whether a mime type is handled by the renderer.
|
||||||
|
*
|
||||||
|
* @param mimeType The mime type to test.
|
||||||
|
* @return True if the renderer can handle the mime type. False otherwise.
|
||||||
|
*/
|
||||||
protected boolean handlesMimeType(String mimeType) {
|
protected boolean handlesMimeType(String mimeType) {
|
||||||
return true;
|
return true;
|
||||||
// TODO: Uncomment once the TODO above is fixed.
|
// TODO: Uncomment once the TODO above is fixed.
|
||||||
|
|
@ -196,6 +202,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected void onEnabled(long timeUs, boolean joining) {
|
protected void onEnabled(long timeUs, boolean joining) {
|
||||||
source.enable(trackIndex, timeUs);
|
source.enable(trackIndex, timeUs);
|
||||||
|
sourceIsReady = false;
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
|
|
@ -280,14 +287,20 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDisabled() {
|
protected void onDisabled() {
|
||||||
releaseCodec();
|
|
||||||
format = null;
|
format = null;
|
||||||
drmInitData = null;
|
drmInitData = null;
|
||||||
if (openedDrmSession) {
|
try {
|
||||||
drmSessionManager.close();
|
releaseCodec();
|
||||||
openedDrmSession = false;
|
} finally {
|
||||||
|
try {
|
||||||
|
if (openedDrmSession) {
|
||||||
|
drmSessionManager.close();
|
||||||
|
openedDrmSession = false;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
source.disable(trackIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
source.disable(trackIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void releaseCodec() {
|
protected void releaseCodec() {
|
||||||
|
|
@ -332,7 +345,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected long getBufferedPositionUs() {
|
protected long getBufferedPositionUs() {
|
||||||
long sourceBufferedPosition = source.getBufferedPositionUs();
|
long sourceBufferedPosition = source.getBufferedPositionUs();
|
||||||
return sourceBufferedPosition == UNKNOWN_TIME || sourceBufferedPosition == END_OF_TRACK
|
return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US
|
||||||
? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
|
? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,6 +353,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||||
currentPositionUs = timeUs;
|
currentPositionUs = timeUs;
|
||||||
source.seekToUs(timeUs);
|
source.seekToUs(timeUs);
|
||||||
|
sourceIsReady = false;
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
|
|
@ -358,7 +372,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
source.continueBuffering(timeUs);
|
sourceIsReady = source.continueBuffering(timeUs);
|
||||||
checkForDiscontinuity();
|
checkForDiscontinuity();
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
readFormat();
|
readFormat();
|
||||||
|
|
@ -373,6 +387,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
while (feedInputBuffer()) {}
|
while (feedInputBuffer()) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
codecCounters.ensureUpdated();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ExoPlaybackException(e);
|
throw new ExoPlaybackException(e);
|
||||||
}
|
}
|
||||||
|
|
@ -394,7 +409,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
if (!sampleHolder.decodeOnly) {
|
if (!sampleHolder.decodeOnly) {
|
||||||
currentPositionUs = sampleHolder.timeUs;
|
currentPositionUs = sampleHolder.timeUs;
|
||||||
}
|
}
|
||||||
codecCounters.discardedSamplesCount++;
|
|
||||||
} else if (result == SampleSource.FORMAT_READ) {
|
} else if (result == SampleSource.FORMAT_READ) {
|
||||||
onInputFormatChanged(formatHolder);
|
onInputFormatChanged(formatHolder);
|
||||||
}
|
}
|
||||||
|
|
@ -467,7 +481,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == SampleSource.NOTHING_READ) {
|
if (result == SampleSource.NOTHING_READ) {
|
||||||
codecCounters.inputBufferWaitingForSampleCount++;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (result == SampleSource.DISCONTINUITY_READ) {
|
if (result == SampleSource.DISCONTINUITY_READ) {
|
||||||
|
|
@ -496,7 +509,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
try {
|
try {
|
||||||
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
codecCounters.queuedEndOfStreamCount++;
|
|
||||||
} catch (CryptoException e) {
|
} catch (CryptoException e) {
|
||||||
notifyCryptoError(e);
|
notifyCryptoError(e);
|
||||||
throw new ExoPlaybackException(e);
|
throw new ExoPlaybackException(e);
|
||||||
|
|
@ -536,10 +548,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
} else {
|
} else {
|
||||||
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
|
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
|
||||||
}
|
}
|
||||||
codecCounters.queuedInputBufferCount++;
|
|
||||||
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
|
||||||
codecCounters.keyframeCount++;
|
|
||||||
}
|
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
||||||
} catch (CryptoException e) {
|
} catch (CryptoException e) {
|
||||||
|
|
@ -625,7 +633,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
* @param newFormat The new format.
|
* @param newFormat The new format.
|
||||||
* @return True if the existing instance can be reconfigured. False otherwise.
|
* @return True if the existing instance can be reconfigured. False otherwise.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
|
||||||
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
|
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
|
||||||
MediaFormat oldFormat, MediaFormat newFormat) {
|
MediaFormat oldFormat, MediaFormat newFormat) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -639,10 +646,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected boolean isReady() {
|
protected boolean isReady() {
|
||||||
return format != null && !waitingForKeys
|
return format != null && !waitingForKeys
|
||||||
&& ((codec == null && !shouldInitCodec()) // We don't want the codec
|
&& (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod());
|
||||||
|| outputIndex >= 0 // Or we have an output buffer ready to release
|
|
||||||
|| inputIndex < 0 // Or we don't have any input buffers to write to
|
|
||||||
|| isWithinHotswapPeriod()); // Or the codec is being hotswapped
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWithinHotswapPeriod() {
|
private boolean isWithinHotswapPeriod() {
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isReady() {
|
protected boolean isReady() {
|
||||||
if (super.isReady() && (renderedFirstFrame || !codecInitialized())) {
|
if (super.isReady()) {
|
||||||
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
||||||
joiningDeadlineUs = -1;
|
joiningDeadlineUs = -1;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -148,12 +148,25 @@ public class MediaFormat {
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MediaFormat other = (MediaFormat) obj;
|
return equalsInternal((MediaFormat) obj, false);
|
||||||
if (maxInputSize != other.maxInputSize || width != other.width || height != other.height ||
|
}
|
||||||
maxWidth != other.maxWidth || maxHeight != other.maxHeight ||
|
|
||||||
channelCount != other.channelCount || sampleRate != other.sampleRate ||
|
public boolean equals(MediaFormat other, boolean ignoreMaxDimensions) {
|
||||||
!Util.areEqual(mimeType, other.mimeType) ||
|
if (this == other) {
|
||||||
initializationData.size() != other.initializationData.size()) {
|
return true;
|
||||||
|
}
|
||||||
|
if (other == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return equalsInternal(other, ignoreMaxDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) {
|
||||||
|
if (maxInputSize != other.maxInputSize || width != other.width || height != other.height
|
||||||
|
|| (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight))
|
||||||
|
|| channelCount != other.channelCount || sampleRate != other.sampleRate
|
||||||
|
|| !Util.areEqual(mimeType, other.mimeType)
|
||||||
|
|| initializationData.size() != other.initializationData.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < initializationData.size(); i++) {
|
for (int i = 0; i < initializationData.size(); i++) {
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,11 @@ public interface SampleSource {
|
||||||
* Indicates to the source that it should still be buffering data.
|
* Indicates to the source that it should still be buffering data.
|
||||||
*
|
*
|
||||||
* @param playbackPositionUs The current playback position.
|
* @param playbackPositionUs The current playback position.
|
||||||
|
* @return True if the source has available samples, or if the end of the stream has been reached.
|
||||||
|
* False if more data needs to be buffered for samples to become available.
|
||||||
|
* @throws IOException If an error occurred reading from the source.
|
||||||
*/
|
*/
|
||||||
public void continueBuffering(long playbackPositionUs);
|
public boolean continueBuffering(long playbackPositionUs) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to read either a sample, a new format or or a discontinuity from the source.
|
* Attempts to read either a sample, a new format or or a discontinuity from the source.
|
||||||
|
|
@ -144,8 +147,8 @@ public interface SampleSource {
|
||||||
* This method should not be called until after the source has been successfully prepared.
|
* This method should not be called until after the source has been successfully prepared.
|
||||||
*
|
*
|
||||||
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
||||||
* or {@link TrackRenderer#END_OF_TRACK} if data is buffered to the end of the stream, or
|
* or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or
|
||||||
* {@link TrackRenderer#UNKNOWN_TIME} if no estimate is available.
|
* {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
|
||||||
*/
|
*/
|
||||||
public long getBufferedPositionUs();
|
public long getBufferedPositionUs();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,16 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
/**
|
/**
|
||||||
* Represents an unknown time or duration.
|
* Represents an unknown time or duration.
|
||||||
*/
|
*/
|
||||||
public static final long UNKNOWN_TIME = -1;
|
public static final long UNKNOWN_TIME_US = -1;
|
||||||
/**
|
/**
|
||||||
* Represents a time or duration that should match the duration of the longest track whose
|
* Represents a time or duration that should match the duration of the longest track whose
|
||||||
* duration is known.
|
* duration is known.
|
||||||
*/
|
*/
|
||||||
public static final long MATCH_LONGEST = -2;
|
public static final long MATCH_LONGEST_US = -2;
|
||||||
/**
|
/**
|
||||||
* Represents the time of the end of the track.
|
* Represents the time of the end of the track.
|
||||||
*/
|
*/
|
||||||
public static final long END_OF_TRACK = -3;
|
public static final long END_OF_TRACK_US = -3;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
|
|
||||||
|
|
@ -110,7 +110,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
*
|
*
|
||||||
* @return The current state (one of the STATE_* constants), for convenience.
|
* @return The current state (one of the STATE_* constants), for convenience.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
|
||||||
/* package */ final int prepare() throws ExoPlaybackException {
|
/* package */ final int prepare() throws ExoPlaybackException {
|
||||||
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
|
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
|
||||||
state = doPrepare();
|
state = doPrepare();
|
||||||
|
|
@ -301,9 +300,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
* This method may be called when the renderer is in the following states:
|
* This method may be called when the renderer is in the following states:
|
||||||
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||||
*
|
*
|
||||||
* @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST} if
|
* @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST_US} if
|
||||||
* the track's duration should match that of the longest track whose duration is known, or
|
* the track's duration should match that of the longest track whose duration is known, or
|
||||||
* or {@link #UNKNOWN_TIME} if the duration is not known.
|
* or {@link #UNKNOWN_TIME_US} if the duration is not known.
|
||||||
*/
|
*/
|
||||||
protected abstract long getDurationUs();
|
protected abstract long getDurationUs();
|
||||||
|
|
||||||
|
|
@ -324,8 +323,8 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||||
*
|
*
|
||||||
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
||||||
* or {@link #END_OF_TRACK} if the track is fully buffered, or {@link #UNKNOWN_TIME} if no
|
* or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if
|
||||||
* estimate is available.
|
* no estimate is available.
|
||||||
*/
|
*/
|
||||||
protected abstract long getBufferedPositionUs();
|
protected abstract long getBufferedPositionUs();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk;
|
package com.google.android.exoplayer.chunk;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.upstream.Allocation;
|
import com.google.android.exoplayer.upstream.Allocation;
|
||||||
import com.google.android.exoplayer.upstream.Allocator;
|
import com.google.android.exoplayer.upstream.Allocator;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
|
@ -51,7 +52,7 @@ public abstract class Chunk implements Loadable {
|
||||||
/**
|
/**
|
||||||
* @param dataSource The source from which the data should be loaded.
|
* @param dataSource The source from which the data should be loaded.
|
||||||
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
|
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
|
||||||
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then
|
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
|
||||||
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
||||||
* {@link Integer#MAX_VALUE}.
|
* {@link Integer#MAX_VALUE}.
|
||||||
* @param format See {@link #format}.
|
* @param format See {@link #format}.
|
||||||
|
|
@ -89,8 +90,8 @@ public abstract class Chunk implements Loadable {
|
||||||
/**
|
/**
|
||||||
* Gets the length of the chunk in bytes.
|
* Gets the length of the chunk in bytes.
|
||||||
*
|
*
|
||||||
* @return The length of the chunk in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length
|
* @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet
|
||||||
* has yet to be determined.
|
* to be determined.
|
||||||
*/
|
*/
|
||||||
public final long getLength() {
|
public final long getLength() {
|
||||||
return dataSourceStream.getLength();
|
return dataSourceStream.getLength();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk;
|
package com.google.android.exoplayer.chunk;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.FormatHolder;
|
import com.google.android.exoplayer.FormatHolder;
|
||||||
import com.google.android.exoplayer.LoadControl;
|
import com.google.android.exoplayer.LoadControl;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
|
|
@ -22,7 +23,6 @@ import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackInfo;
|
import com.google.android.exoplayer.TrackInfo;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
|
||||||
import com.google.android.exoplayer.upstream.Loader;
|
import com.google.android.exoplayer.upstream.Loader;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
|
|
@ -57,24 +57,27 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
* load is for initialization data.
|
* load is for initialization data.
|
||||||
* @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
|
* @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
|
||||||
* load is for initialization data.
|
* load is for initialization data.
|
||||||
* @param totalBytes The length of the data being loaded in bytes.
|
* @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if
|
||||||
|
* the length of the data has not yet been determined.
|
||||||
*/
|
*/
|
||||||
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
int mediaStartTimeMs, int mediaEndTimeMs, long length);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when the current load operation completes.
|
* Invoked when the current load operation completes.
|
||||||
*
|
*
|
||||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||||
|
* @param bytesLoaded The number of bytes that were loaded.
|
||||||
*/
|
*/
|
||||||
void onLoadCompleted(int sourceId);
|
void onLoadCompleted(int sourceId, long bytesLoaded);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when the current upstream load operation is canceled.
|
* Invoked when the current upstream load operation is canceled.
|
||||||
*
|
*
|
||||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||||
|
* @param bytesLoaded The number of bytes that were loaded prior to the cancellation.
|
||||||
*/
|
*/
|
||||||
void onLoadCanceled(int sourceId);
|
void onLoadCanceled(int sourceId, long bytesLoaded);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when data is removed from the back of the buffer, typically so that it can be
|
* Invoked when data is removed from the back of the buffer, typically so that it can be
|
||||||
|
|
@ -83,10 +86,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||||
* @param mediaStartTimeMs The media time of the start of the discarded data.
|
* @param mediaStartTimeMs The media time of the start of the discarded data.
|
||||||
* @param mediaEndTimeMs The media time of the end of the discarded data.
|
* @param mediaEndTimeMs The media time of the end of the discarded data.
|
||||||
* @param totalBytes The length of the data being discarded in bytes.
|
* @param bytesDiscarded The length of the data being discarded in bytes.
|
||||||
*/
|
*/
|
||||||
void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
||||||
long totalBytes);
|
long bytesDiscarded);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when an error occurs loading media data.
|
* Invoked when an error occurs loading media data.
|
||||||
|
|
@ -111,10 +114,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||||
* @param mediaStartTimeMs The media time of the start of the discarded data.
|
* @param mediaStartTimeMs The media time of the start of the discarded data.
|
||||||
* @param mediaEndTimeMs The media time of the end of the discarded data.
|
* @param mediaEndTimeMs The media time of the end of the discarded data.
|
||||||
* @param totalBytes The length of the data being discarded in bytes.
|
* @param bytesDiscarded The length of the data being discarded in bytes.
|
||||||
*/
|
*/
|
||||||
void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
||||||
long totalBytes);
|
long bytesDiscarded);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when the downstream format changes (i.e. when the format being supplied to the
|
* Invoked when the downstream format changes (i.e. when the format being supplied to the
|
||||||
|
|
@ -246,11 +249,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void continueBuffering(long playbackPositionUs) {
|
public boolean continueBuffering(long playbackPositionUs) throws IOException {
|
||||||
Assertions.checkState(state == STATE_ENABLED);
|
Assertions.checkState(state == STATE_ENABLED);
|
||||||
downstreamPositionUs = playbackPositionUs;
|
downstreamPositionUs = playbackPositionUs;
|
||||||
chunkSource.continueBuffering(playbackPositionUs);
|
chunkSource.continueBuffering(playbackPositionUs);
|
||||||
updateLoadControl();
|
updateLoadControl();
|
||||||
|
if (isPendingReset() || mediaChunks.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
} else if (mediaChunks.getFirst().sampleAvailable()) {
|
||||||
|
// There's a sample available to be read from the current chunk.
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// It may be the case that the current chunk has been fully read but not yet discarded and
|
||||||
|
// that the next chunk has an available sample. Return true if so, otherwise false.
|
||||||
|
return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -309,7 +322,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
|
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
|
||||||
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) {
|
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
|
||||||
chunkSource.getMaxVideoDimensions(mediaFormat);
|
chunkSource.getMaxVideoDimensions(mediaFormat);
|
||||||
formatHolder.format = mediaFormat;
|
formatHolder.format = mediaFormat;
|
||||||
formatHolder.drmInitData = mediaChunk.getPsshInfo();
|
formatHolder.drmInitData = mediaChunk.getPsshInfo();
|
||||||
|
|
@ -373,14 +386,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
if (currentLoadable != null && mediaChunk == currentLoadable) {
|
if (currentLoadable != null && mediaChunk == currentLoadable) {
|
||||||
// Linearly interpolate partially-fetched chunk times.
|
// Linearly interpolate partially-fetched chunk times.
|
||||||
long chunkLength = mediaChunk.getLength();
|
long chunkLength = mediaChunk.getLength();
|
||||||
if (chunkLength != DataSpec.LENGTH_UNBOUNDED) {
|
if (chunkLength != C.LENGTH_UNBOUNDED) {
|
||||||
return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
|
return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
|
||||||
mediaChunk.bytesLoaded()) / chunkLength;
|
mediaChunk.bytesLoaded()) / chunkLength;
|
||||||
} else {
|
} else {
|
||||||
return mediaChunk.startTimeUs;
|
return mediaChunk.startTimeUs;
|
||||||
}
|
}
|
||||||
} else if (mediaChunk.isLastChunk()) {
|
} else if (mediaChunk.isLastChunk()) {
|
||||||
return TrackRenderer.END_OF_TRACK;
|
return TrackRenderer.END_OF_TRACK_US;
|
||||||
} else {
|
} else {
|
||||||
return mediaChunk.endTimeUs;
|
return mediaChunk.endTimeUs;
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +412,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
@Override
|
@Override
|
||||||
public void onLoaded() {
|
public void onLoaded() {
|
||||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||||
|
notifyLoadCompleted(currentLoadable.bytesLoaded());
|
||||||
try {
|
try {
|
||||||
currentLoadable.consume();
|
currentLoadable.consume();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -414,7 +428,6 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
if (!currentLoadableExceptionFatal) {
|
if (!currentLoadableExceptionFatal) {
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
}
|
}
|
||||||
notifyLoadCompleted();
|
|
||||||
updateLoadControl();
|
updateLoadControl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -422,11 +435,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
@Override
|
@Override
|
||||||
public void onCanceled() {
|
public void onCanceled() {
|
||||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||||
|
notifyLoadCanceled(currentLoadable.bytesLoaded());
|
||||||
if (!isMediaChunk(currentLoadable)) {
|
if (!isMediaChunk(currentLoadable)) {
|
||||||
currentLoadable.release();
|
currentLoadable.release();
|
||||||
}
|
}
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
notifyLoadCanceled();
|
|
||||||
if (state == STATE_ENABLED) {
|
if (state == STATE_ENABLED) {
|
||||||
restartFrom(pendingResetTime);
|
restartFrom(pendingResetTime);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -667,35 +680,35 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
|
|
||||||
private void notifyLoadStarted(final String formatId, final int trigger,
|
private void notifyLoadStarted(final String formatId, final int trigger,
|
||||||
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
|
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
|
||||||
final long totalBytes) {
|
final long length) {
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization,
|
eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization,
|
||||||
usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), totalBytes);
|
usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), length);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyLoadCompleted() {
|
private void notifyLoadCompleted(final long bytesLoaded) {
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
eventListener.onLoadCompleted(eventSourceId);
|
eventListener.onLoadCompleted(eventSourceId, bytesLoaded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyLoadCanceled() {
|
private void notifyLoadCanceled(final long bytesLoaded) {
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
eventListener.onLoadCanceled(eventSourceId);
|
eventListener.onLoadCanceled(eventSourceId, bytesLoaded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -750,13 +763,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
|
private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
|
||||||
final long totalBytes) {
|
final long bytesDiscarded) {
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
|
eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
|
||||||
usToMs(mediaEndTimeUs), totalBytes);
|
usToMs(mediaEndTimeUs), bytesDiscarded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@ public class Format {
|
||||||
*/
|
*/
|
||||||
public final int bitrate;
|
public final int bitrate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The language of the format. Can be null if unknown.
|
||||||
|
* <p>
|
||||||
|
* The language codes are two-letter lowercase ISO language codes (such as "en") as defined by
|
||||||
|
* ISO 639-1.
|
||||||
|
*/
|
||||||
|
public final String language;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The average bandwidth in bytes per second.
|
* The average bandwidth in bytes per second.
|
||||||
*
|
*
|
||||||
|
|
@ -90,6 +98,21 @@ public class Format {
|
||||||
*/
|
*/
|
||||||
public Format(String id, String mimeType, int width, int height, int numChannels,
|
public Format(String id, String mimeType, int width, int height, int numChannels,
|
||||||
int audioSamplingRate, int bitrate) {
|
int audioSamplingRate, int bitrate) {
|
||||||
|
this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id The format identifier.
|
||||||
|
* @param mimeType The format mime type.
|
||||||
|
* @param width The width of the video in pixels, or -1 for non-video formats.
|
||||||
|
* @param height The height of the video in pixels, or -1 for non-video formats.
|
||||||
|
* @param numChannels The number of audio channels, or -1 for non-audio formats.
|
||||||
|
* @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
|
||||||
|
* @param bitrate The average bandwidth of the format in bits per second.
|
||||||
|
* @param language The language of the format.
|
||||||
|
*/
|
||||||
|
public Format(String id, String mimeType, int width, int height, int numChannels,
|
||||||
|
int audioSamplingRate, int bitrate, String language) {
|
||||||
this.id = Assertions.checkNotNull(id);
|
this.id = Assertions.checkNotNull(id);
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
|
|
@ -97,6 +120,7 @@ public class Format {
|
||||||
this.numChannels = numChannels;
|
this.numChannels = numChannels;
|
||||||
this.audioSamplingRate = audioSamplingRate;
|
this.audioSamplingRate = audioSamplingRate;
|
||||||
this.bitrate = bitrate;
|
this.bitrate = bitrate;
|
||||||
|
this.language = language;
|
||||||
this.bandwidth = bitrate / 8;
|
this.bandwidth = bitrate / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ public interface FormatEvaluator {
|
||||||
*/
|
*/
|
||||||
public static class AdaptiveEvaluator implements FormatEvaluator {
|
public static class AdaptiveEvaluator implements FormatEvaluator {
|
||||||
|
|
||||||
public static final int DEFAULT_MAX_INITIAL_BYTE_RATE = 100000;
|
public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
|
||||||
|
|
||||||
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
|
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
|
||||||
public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
|
public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
|
||||||
|
|
@ -173,7 +173,7 @@ public interface FormatEvaluator {
|
||||||
|
|
||||||
private final BandwidthMeter bandwidthMeter;
|
private final BandwidthMeter bandwidthMeter;
|
||||||
|
|
||||||
private final int maxInitialByteRate;
|
private final int maxInitialBitrate;
|
||||||
private final long minDurationForQualityIncreaseUs;
|
private final long minDurationForQualityIncreaseUs;
|
||||||
private final long maxDurationForQualityDecreaseUs;
|
private final long maxDurationForQualityDecreaseUs;
|
||||||
private final long minDurationToRetainAfterDiscardUs;
|
private final long minDurationToRetainAfterDiscardUs;
|
||||||
|
|
@ -183,7 +183,7 @@ public interface FormatEvaluator {
|
||||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||||
*/
|
*/
|
||||||
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) {
|
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) {
|
||||||
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BYTE_RATE,
|
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
|
||||||
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||||
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||||
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
|
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
|
||||||
|
|
@ -191,7 +191,7 @@ public interface FormatEvaluator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||||
* @param maxInitialByteRate The maximum bandwidth in bytes per second that should be assumed
|
* @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
|
||||||
* when bandwidthMeter cannot provide an estimate due to playback having only just started.
|
* when bandwidthMeter cannot provide an estimate due to playback having only just started.
|
||||||
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
|
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
|
||||||
* the evaluator to consider switching to a higher quality format.
|
* the evaluator to consider switching to a higher quality format.
|
||||||
|
|
@ -206,13 +206,13 @@ public interface FormatEvaluator {
|
||||||
* for inaccuracies in the bandwidth estimator.
|
* for inaccuracies in the bandwidth estimator.
|
||||||
*/
|
*/
|
||||||
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
|
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
|
||||||
int maxInitialByteRate,
|
int maxInitialBitrate,
|
||||||
int minDurationForQualityIncreaseMs,
|
int minDurationForQualityIncreaseMs,
|
||||||
int maxDurationForQualityDecreaseMs,
|
int maxDurationForQualityDecreaseMs,
|
||||||
int minDurationToRetainAfterDiscardMs,
|
int minDurationToRetainAfterDiscardMs,
|
||||||
float bandwidthFraction) {
|
float bandwidthFraction) {
|
||||||
this.bandwidthMeter = bandwidthMeter;
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
this.maxInitialByteRate = maxInitialByteRate;
|
this.maxInitialBitrate = maxInitialBitrate;
|
||||||
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
||||||
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
||||||
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
||||||
|
|
@ -235,7 +235,7 @@ public interface FormatEvaluator {
|
||||||
long bufferedDurationUs = queue.isEmpty() ? 0
|
long bufferedDurationUs = queue.isEmpty() ? 0
|
||||||
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
|
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
|
||||||
Format current = evaluation.format;
|
Format current = evaluation.format;
|
||||||
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
|
Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate());
|
||||||
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
|
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
|
||||||
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
|
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
|
||||||
if (isHigher) {
|
if (isHigher) {
|
||||||
|
|
@ -276,11 +276,11 @@ public interface FormatEvaluator {
|
||||||
/**
|
/**
|
||||||
* Compute the ideal format ignoring buffer health.
|
* Compute the ideal format ignoring buffer health.
|
||||||
*/
|
*/
|
||||||
protected Format determineIdealFormat(Format[] formats, long bandwidthEstimate) {
|
protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
|
||||||
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
|
long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate);
|
||||||
for (int i = 0; i < formats.length; i++) {
|
for (int i = 0; i < formats.length; i++) {
|
||||||
Format format = formats[i];
|
Format format = formats[i];
|
||||||
if ((format.bitrate / 8) <= effectiveBandwidth) {
|
if (format.bitrate <= effectiveBitrate) {
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -291,9 +291,9 @@ public interface FormatEvaluator {
|
||||||
/**
|
/**
|
||||||
* Apply overhead factor, or default value in absence of estimate.
|
* Apply overhead factor, or default value in absence of estimate.
|
||||||
*/
|
*/
|
||||||
protected long computeEffectiveBandwidthEstimate(long bandwidthEstimate) {
|
protected long computeEffectiveBitrateEstimate(long bitrateEstimate) {
|
||||||
return bandwidthEstimate == BandwidthMeter.NO_ESTIMATE
|
return bitrateEstimate == BandwidthMeter.NO_ESTIMATE
|
||||||
? maxInitialByteRate : (long) (bandwidthEstimate * bandwidthFraction);
|
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,14 @@ public abstract class MediaChunk extends Chunk {
|
||||||
*/
|
*/
|
||||||
public abstract boolean prepare() throws ParserException;
|
public abstract boolean prepare() throws ParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the next sample is available.
|
||||||
|
*
|
||||||
|
* @return True if the next sample is available for reading. False otherwise.
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
public abstract boolean sampleAvailable() throws ParserException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the next media sample from the chunk.
|
* Reads the next media sample from the chunk.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
||||||
|
|
@ -103,12 +103,19 @@ public final class Mp4MediaChunk extends MediaChunk {
|
||||||
return prepared;
|
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
|
@Override
|
||||||
public boolean read(SampleHolder holder) throws ParserException {
|
public boolean read(SampleHolder holder) throws ParserException {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
Assertions.checkState(inputStream != null);
|
Assertions.checkState(inputStream != null);
|
||||||
int result = extractor.read(inputStream, holder);
|
int result = extractor.read(inputStream, holder);
|
||||||
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE_FULL) != 0;
|
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
|
||||||
if (sampleRead) {
|
if (sampleRead) {
|
||||||
holder.timeUs -= sampleOffsetUs;
|
holder.timeUs -= sampleOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,16 @@ public class SingleSampleMediaChunk extends MediaChunk {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sampleAvailable() {
|
||||||
|
return isLoadFinished() && !isReadFinished();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean read(SampleHolder holder) {
|
public boolean read(SampleHolder holder) {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
Assertions.checkState(inputStream != null);
|
Assertions.checkState(inputStream != null);
|
||||||
if (!isLoadFinished()) {
|
if (!sampleAvailable()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int bytesLoaded = (int) bytesLoaded();
|
int bytesLoaded = (int) bytesLoaded();
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer.chunk;
|
package com.google.android.exoplayer.chunk;
|
||||||
|
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
|
@ -69,11 +70,19 @@ public final class WebmMediaChunk extends MediaChunk {
|
||||||
return true;
|
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
|
@Override
|
||||||
public boolean read(SampleHolder holder) {
|
public boolean read(SampleHolder holder) {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
Assertions.checkState(inputStream != null);
|
Assertions.checkState(inputStream != null);
|
||||||
return extractor.read(inputStream, holder);
|
int result = extractor.read(inputStream, holder);
|
||||||
|
return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||||
|
|
||||||
RangedUri pendingInitializationUri = null;
|
RangedUri pendingInitializationUri = null;
|
||||||
RangedUri pendingIndexUri = null;
|
RangedUri pendingIndexUri = null;
|
||||||
if (extractor.getTrack() == null) {
|
if (extractor.getFormat() == null) {
|
||||||
pendingInitializationUri = selectedRepresentation.getInitializationUri();
|
pendingInitializationUri = selectedRepresentation.getInitializationUri();
|
||||||
}
|
}
|
||||||
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
|
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
|
||||||
|
|
@ -199,10 +199,10 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||||
if (initializationUri != null) {
|
if (initializationUri != null) {
|
||||||
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
||||||
// the two requests together to request both at once.
|
// the two requests together to request both at once.
|
||||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV;
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
|
||||||
requestUri = initializationUri.attemptMerge(indexUri);
|
requestUri = initializationUri.attemptMerge(indexUri);
|
||||||
if (requestUri != null) {
|
if (requestUri != null) {
|
||||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
|
||||||
indexAnchor = indexUri.start + indexUri.length;
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
} else {
|
} else {
|
||||||
requestUri = initializationUri;
|
requestUri = initializationUri;
|
||||||
|
|
@ -210,7 +210,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||||
} else {
|
} else {
|
||||||
requestUri = indexUri;
|
requestUri = indexUri;
|
||||||
indexAnchor = indexUri.start + indexUri.length;
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
|
||||||
}
|
}
|
||||||
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
|
|
@ -256,9 +256,9 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||||
throw new ParserException("Invalid extractor result. Expected "
|
throw new ParserException("Invalid extractor result. Expected "
|
||||||
+ expectedExtractorResult + ", got " + result);
|
+ expectedExtractorResult + ", got " + result);
|
||||||
}
|
}
|
||||||
if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) {
|
if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
|
||||||
segmentIndexes.put(format.id,
|
segmentIndexes.put(format.id,
|
||||||
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor));
|
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,21 +56,26 @@ public class DashWebmChunkSource implements ChunkSource {
|
||||||
|
|
||||||
private final Format[] formats;
|
private final Format[] formats;
|
||||||
private final HashMap<String, Representation> representations;
|
private final HashMap<String, Representation> representations;
|
||||||
private final HashMap<String, WebmExtractor> extractors;
|
private final HashMap<String, DefaultWebmExtractor> extractors;
|
||||||
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||||
|
* @param evaluator Selects from the available formats.
|
||||||
|
* @param representations The representations to be considered by the source.
|
||||||
|
*/
|
||||||
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||||
Representation... representations) {
|
Representation... representations) {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = evaluator;
|
this.evaluator = evaluator;
|
||||||
this.formats = new Format[representations.length];
|
this.formats = new Format[representations.length];
|
||||||
this.extractors = new HashMap<String, WebmExtractor>();
|
this.extractors = new HashMap<String, DefaultWebmExtractor>();
|
||||||
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||||
this.representations = new HashMap<String, Representation>();
|
this.representations = new HashMap<String, Representation>();
|
||||||
this.trackInfo = new TrackInfo(
|
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||||
representations[0].format.mimeType, representations[0].periodDurationMs * 1000);
|
representations[0].periodDurationMs * 1000);
|
||||||
this.evaluation = new Evaluation();
|
this.evaluation = new Evaluation();
|
||||||
int maxWidth = 0;
|
int maxWidth = 0;
|
||||||
int maxHeight = 0;
|
int maxHeight = 0;
|
||||||
|
|
@ -109,7 +114,7 @@ public class DashWebmChunkSource implements ChunkSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable(List<? extends MediaChunk> queue) {
|
public void disable(List<? extends MediaChunk> queue) {
|
||||||
evaluator.disable();
|
evaluator.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -140,13 +145,18 @@ public class DashWebmChunkSource implements ChunkSource {
|
||||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||||
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||||
|
|
||||||
if (!extractor.isPrepared()) {
|
RangedUri pendingInitializationUri = null;
|
||||||
// TODO: This code forces cues to exist and to immediately follow the initialization
|
RangedUri pendingIndexUri = null;
|
||||||
// data. Webm extractor should be generalized to allow cues to be optional. See [redacted].
|
if (extractor.getFormat() == null) {
|
||||||
RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge(
|
pendingInitializationUri = selectedRepresentation.getInitializationUri();
|
||||||
selectedRepresentation.getIndexUri());
|
}
|
||||||
Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation,
|
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
|
||||||
extractor, dataSource, evaluation.trigger);
|
pendingIndexUri = selectedRepresentation.getIndexUri();
|
||||||
|
}
|
||||||
|
if (pendingInitializationUri != null || pendingIndexUri != null) {
|
||||||
|
// We have initialization and/or index requests to make.
|
||||||
|
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
|
||||||
|
selectedRepresentation, extractor, dataSource, evaluation.trigger);
|
||||||
lastChunkWasInitialization = true;
|
lastChunkWasInitialization = true;
|
||||||
out.chunk = initializationChunk;
|
out.chunk = initializationChunk;
|
||||||
return;
|
return;
|
||||||
|
|
@ -181,12 +191,29 @@ public class DashWebmChunkSource implements ChunkSource {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation,
|
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||||
WebmExtractor extractor, DataSource dataSource, int trigger) {
|
Representation representation, WebmExtractor extractor, DataSource dataSource,
|
||||||
DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start,
|
int trigger) {
|
||||||
initializationUri.length, representation.getCacheKey());
|
int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM;
|
||||||
|
RangedUri requestUri;
|
||||||
|
if (initializationUri != null) {
|
||||||
|
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
||||||
|
// the two requests together to request both at once.
|
||||||
|
expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT;
|
||||||
|
requestUri = initializationUri.attemptMerge(indexUri);
|
||||||
|
if (requestUri != null) {
|
||||||
|
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
|
||||||
|
} else {
|
||||||
|
requestUri = initializationUri;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestUri = indexUri;
|
||||||
|
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
|
||||||
|
}
|
||||||
|
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||||
|
representation.getCacheKey());
|
||||||
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
|
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
|
||||||
extractor);
|
extractor, expectedExtractorResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||||
|
|
@ -206,22 +233,27 @@ public class DashWebmChunkSource implements ChunkSource {
|
||||||
private class InitializationWebmLoadable extends Chunk {
|
private class InitializationWebmLoadable extends Chunk {
|
||||||
|
|
||||||
private final WebmExtractor extractor;
|
private final WebmExtractor extractor;
|
||||||
|
private final int expectedExtractorResult;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
||||||
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||||
Format format, WebmExtractor extractor) {
|
Format format, WebmExtractor extractor, int expectedExtractorResult) {
|
||||||
super(dataSource, dataSpec, format, trigger);
|
super(dataSource, dataSpec, format, trigger);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
|
this.expectedExtractorResult = expectedExtractorResult;
|
||||||
this.uri = dataSpec.uri;
|
this.uri = dataSpec.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||||
extractor.read(stream, null);
|
int result = extractor.read(stream, null);
|
||||||
if (!extractor.isPrepared()) {
|
if (result != expectedExtractorResult) {
|
||||||
throw new ParserException("Invalid initialization data");
|
throw new ParserException("Invalid extractor result. Expected "
|
||||||
|
+ expectedExtractorResult + ", got " + result);
|
||||||
|
}
|
||||||
|
if ((result & WebmExtractor.RESULT_READ_INDEX) != 0) {
|
||||||
|
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0));
|
||||||
}
|
}
|
||||||
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
|
|
||||||
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
||||||
|
String language = xpp.getAttributeValue(null, "lang");
|
||||||
int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
|
int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
|
||||||
|
|
||||||
int id = -1;
|
int id = -1;
|
||||||
|
|
@ -160,7 +161,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||||
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
|
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
|
||||||
} else if (isStartTag(xpp, "Representation")) {
|
} else if (isStartTag(xpp, "Representation")) {
|
||||||
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
|
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
|
||||||
periodDurationMs, mimeType, segmentBase);
|
periodDurationMs, mimeType, language, segmentBase);
|
||||||
contentType = checkAdaptationSetTypeConsistency(contentType,
|
contentType = checkAdaptationSetTypeConsistency(contentType,
|
||||||
parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
|
parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
|
||||||
representations.add(representation);
|
representations.add(representation);
|
||||||
|
|
@ -230,8 +231,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||||
// Representation parsing.
|
// Representation parsing.
|
||||||
|
|
||||||
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
|
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
|
||||||
long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase)
|
long periodStartMs, long periodDurationMs, String mimeType, String language,
|
||||||
throws XmlPullParserException, IOException {
|
SegmentBase segmentBase) throws XmlPullParserException, IOException {
|
||||||
String id = xpp.getAttributeValue(null, "id");
|
String id = xpp.getAttributeValue(null, "id");
|
||||||
int bandwidth = parseInt(xpp, "bandwidth");
|
int bandwidth = parseInt(xpp, "bandwidth");
|
||||||
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
|
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
|
||||||
|
|
@ -257,7 +258,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||||
} while (!isEndTag(xpp, "Representation"));
|
} while (!isEndTag(xpp, "Representation"));
|
||||||
|
|
||||||
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
|
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
|
||||||
bandwidth);
|
bandwidth, language);
|
||||||
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
|
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
|
||||||
segmentBase);
|
segmentBase);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
package com.google.android.exoplayer.parser.mp4;
|
package com.google.android.exoplayer.parser.mp4;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/* package */ abstract class Atom {
|
/* package */ abstract class Atom {
|
||||||
|
|
||||||
|
|
@ -24,7 +23,6 @@ import java.util.List;
|
||||||
public static final int TYPE_avc3 = 0x61766333;
|
public static final int TYPE_avc3 = 0x61766333;
|
||||||
public static final int TYPE_esds = 0x65736473;
|
public static final int TYPE_esds = 0x65736473;
|
||||||
public static final int TYPE_mdat = 0x6D646174;
|
public static final int TYPE_mdat = 0x6D646174;
|
||||||
public static final int TYPE_mfhd = 0x6D666864;
|
|
||||||
public static final int TYPE_mp4a = 0x6D703461;
|
public static final int TYPE_mp4a = 0x6D703461;
|
||||||
public static final int TYPE_tfdt = 0x74666474;
|
public static final int TYPE_tfdt = 0x74666474;
|
||||||
public static final int TYPE_tfhd = 0x74666864;
|
public static final int TYPE_tfhd = 0x74666864;
|
||||||
|
|
@ -54,6 +52,7 @@ import java.util.List;
|
||||||
public static final int TYPE_frma = 0x66726D61;
|
public static final int TYPE_frma = 0x66726D61;
|
||||||
public static final int TYPE_saiz = 0x7361697A;
|
public static final int TYPE_saiz = 0x7361697A;
|
||||||
public static final int TYPE_uuid = 0x75756964;
|
public static final int TYPE_uuid = 0x75756964;
|
||||||
|
public static final int TYPE_senc = 0x73656E63;
|
||||||
|
|
||||||
public final int type;
|
public final int type;
|
||||||
|
|
||||||
|
|
@ -63,17 +62,13 @@ import java.util.List;
|
||||||
|
|
||||||
public final static class LeafAtom extends Atom {
|
public final static class LeafAtom extends Atom {
|
||||||
|
|
||||||
private final ParsableByteArray data;
|
public final ParsableByteArray data;
|
||||||
|
|
||||||
public LeafAtom(int type, ParsableByteArray data) {
|
public LeafAtom(int type, ParsableByteArray data) {
|
||||||
super(type);
|
super(type);
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParsableByteArray getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class ContainerAtom extends Atom {
|
public final static class ContainerAtom extends Atom {
|
||||||
|
|
@ -90,7 +85,8 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LeafAtom getLeafAtomOfType(int type) {
|
public LeafAtom getLeafAtomOfType(int type) {
|
||||||
for (int i = 0; i < children.size(); i++) {
|
int childrenSize = children.size();
|
||||||
|
for (int i = 0; i < childrenSize; i++) {
|
||||||
Atom atom = children.get(i);
|
Atom atom = children.get(i);
|
||||||
if (atom.type == type) {
|
if (atom.type == type) {
|
||||||
return (LeafAtom) atom;
|
return (LeafAtom) atom;
|
||||||
|
|
@ -100,7 +96,8 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContainerAtom getContainerAtomOfType(int type) {
|
public ContainerAtom getContainerAtomOfType(int type) {
|
||||||
for (int i = 0; i < children.size(); i++) {
|
int childrenSize = children.size();
|
||||||
|
for (int i = 0; i < childrenSize; i++) {
|
||||||
Atom atom = children.get(i);
|
Atom atom = children.get(i);
|
||||||
if (atom.type == type) {
|
if (atom.type == type) {
|
||||||
return (ContainerAtom) atom;
|
return (ContainerAtom) atom;
|
||||||
|
|
@ -109,10 +106,6 @@ import java.util.List;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Atom> getChildren() {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* Provides static utility methods for manipulating various types of codec specific data.
|
* Provides static utility methods for manipulating various types of codec specific data.
|
||||||
*/
|
*/
|
||||||
public class CodecSpecificDataUtil {
|
public final class CodecSpecificDataUtil {
|
||||||
|
|
||||||
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public final class FragmentedMp4Extractor {
|
||||||
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
|
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;
|
public static final int RESULT_NEED_MORE_DATA = 1;
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,27 +69,23 @@ public final class FragmentedMp4Extractor {
|
||||||
/**
|
/**
|
||||||
* A media sample was read.
|
* 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()},
|
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
|
||||||
* {@link #getFormat()} and {@link #getPsshInfo}.
|
|
||||||
*/
|
*/
|
||||||
public static final int RESULT_READ_MOOV = 16;
|
public static final int RESULT_READ_INDEX = 16;
|
||||||
/**
|
|
||||||
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
|
|
||||||
*/
|
|
||||||
public static final int RESULT_READ_SIDX = 32;
|
|
||||||
/**
|
/**
|
||||||
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
|
* 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
|
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[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
||||||
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
|
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};
|
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
|
// Parser states
|
||||||
private static final int STATE_READING_ATOM_HEADER = 0;
|
private static final int STATE_READING_ATOM_HEADER = 0;
|
||||||
private static final int STATE_READING_ATOM_PAYLOAD = 1;
|
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_ENCRYPTION_DATA = 2;
|
||||||
private static final int STATE_READING_SAMPLE_START = 3;
|
private static final int STATE_READING_SAMPLE = 3;
|
||||||
private static final int STATE_READING_SAMPLE_INCREMENTAL = 4;
|
|
||||||
|
|
||||||
// Atom data offsets
|
// Atom data offsets
|
||||||
private static final int ATOM_HEADER_SIZE = 8;
|
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_hdlr);
|
||||||
parsedAtoms.add(Atom.TYPE_mdat);
|
parsedAtoms.add(Atom.TYPE_mdat);
|
||||||
parsedAtoms.add(Atom.TYPE_mdhd);
|
parsedAtoms.add(Atom.TYPE_mdhd);
|
||||||
parsedAtoms.add(Atom.TYPE_mfhd);
|
|
||||||
parsedAtoms.add(Atom.TYPE_moof);
|
parsedAtoms.add(Atom.TYPE_moof);
|
||||||
parsedAtoms.add(Atom.TYPE_moov);
|
parsedAtoms.add(Atom.TYPE_moov);
|
||||||
parsedAtoms.add(Atom.TYPE_mp4a);
|
parsedAtoms.add(Atom.TYPE_mp4a);
|
||||||
|
|
@ -135,6 +129,7 @@ public final class FragmentedMp4Extractor {
|
||||||
parsedAtoms.add(Atom.TYPE_pssh);
|
parsedAtoms.add(Atom.TYPE_pssh);
|
||||||
parsedAtoms.add(Atom.TYPE_saiz);
|
parsedAtoms.add(Atom.TYPE_saiz);
|
||||||
parsedAtoms.add(Atom.TYPE_uuid);
|
parsedAtoms.add(Atom.TYPE_uuid);
|
||||||
|
parsedAtoms.add(Atom.TYPE_senc);
|
||||||
PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
|
PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,8 +153,10 @@ public final class FragmentedMp4Extractor {
|
||||||
|
|
||||||
// Parser state
|
// Parser state
|
||||||
private final ParsableByteArray atomHeader;
|
private final ParsableByteArray atomHeader;
|
||||||
|
private final byte[] extendedTypeScratch;
|
||||||
private final Stack<ContainerAtom> containerAtoms;
|
private final Stack<ContainerAtom> containerAtoms;
|
||||||
private final Stack<Integer> containerAtomEndPoints;
|
private final Stack<Integer> containerAtomEndPoints;
|
||||||
|
private final TrackFragment fragmentRun;
|
||||||
|
|
||||||
private int parserState;
|
private int parserState;
|
||||||
private int atomBytesRead;
|
private int atomBytesRead;
|
||||||
|
|
@ -167,9 +164,6 @@ public final class FragmentedMp4Extractor {
|
||||||
private int atomType;
|
private int atomType;
|
||||||
private int atomSize;
|
private int atomSize;
|
||||||
private ParsableByteArray atomData;
|
private ParsableByteArray atomData;
|
||||||
private ParsableByteArray cencAuxiliaryData;
|
|
||||||
private int cencAuxiliaryBytesRead;
|
|
||||||
private int sampleBytesRead;
|
|
||||||
|
|
||||||
private int pendingSeekTimeMs;
|
private int pendingSeekTimeMs;
|
||||||
private int sampleIndex;
|
private int sampleIndex;
|
||||||
|
|
@ -182,9 +176,6 @@ public final class FragmentedMp4Extractor {
|
||||||
private Track track;
|
private Track track;
|
||||||
private DefaultSampleValues extendsDefaults;
|
private DefaultSampleValues extendsDefaults;
|
||||||
|
|
||||||
// Data parsed from the most recent moof atom
|
|
||||||
private TrackFragment fragmentRun;
|
|
||||||
|
|
||||||
public FragmentedMp4Extractor() {
|
public FragmentedMp4Extractor() {
|
||||||
this(0);
|
this(0);
|
||||||
}
|
}
|
||||||
|
|
@ -197,8 +188,10 @@ public final class FragmentedMp4Extractor {
|
||||||
this.workaroundFlags = workaroundFlags;
|
this.workaroundFlags = workaroundFlags;
|
||||||
parserState = STATE_READING_ATOM_HEADER;
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
|
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
|
||||||
|
extendedTypeScratch = new byte[16];
|
||||||
containerAtoms = new Stack<ContainerAtom>();
|
containerAtoms = new Stack<ContainerAtom>();
|
||||||
containerAtomEndPoints = new Stack<Integer>();
|
containerAtomEndPoints = new Stack<Integer>();
|
||||||
|
fragmentRun = new TrackFragment();
|
||||||
psshData = new HashMap<UUID, byte[]>();
|
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.
|
* @return The segment index, or null if a SIDX atom has yet to be parsed.
|
||||||
*/
|
*/
|
||||||
public SegmentIndex getSegmentIndex() {
|
public SegmentIndex getIndex() {
|
||||||
return segmentIndex;
|
return segmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,17 +238,7 @@ public final class FragmentedMp4Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the track information parsed from the stream.
|
* Sideloads track information into the extractor.
|
||||||
*
|
|
||||||
* @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()}.
|
|
||||||
*
|
*
|
||||||
* @param track The track to sideload.
|
* @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
|
* 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
|
* 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.
|
* 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 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
|
* @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:
|
case STATE_READING_ATOM_PAYLOAD:
|
||||||
results |= readAtomPayload(inputStream);
|
results |= readAtomPayload(inputStream);
|
||||||
break;
|
break;
|
||||||
case STATE_READING_CENC_AUXILIARY_DATA:
|
case STATE_READING_ENCRYPTION_DATA:
|
||||||
results |= readCencAuxiliaryData(inputStream);
|
results |= readEncryptionData(inputStream);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
results |= readOrSkipSample(inputStream, out);
|
results |= readOrSkipSample(inputStream, out);
|
||||||
|
|
@ -350,19 +329,13 @@ public final class FragmentedMp4Extractor {
|
||||||
rootAtomBytesRead = 0;
|
rootAtomBytesRead = 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case STATE_READING_CENC_AUXILIARY_DATA:
|
|
||||||
cencAuxiliaryBytesRead = 0;
|
|
||||||
break;
|
|
||||||
case STATE_READING_SAMPLE_START:
|
|
||||||
sampleBytesRead = 0;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
parserState = state;
|
parserState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readAtomHeader(NonBlockingInputStream inputStream) {
|
private int readAtomHeader(NonBlockingInputStream inputStream) {
|
||||||
int remainingBytes = ATOM_HEADER_SIZE - atomBytesRead;
|
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) {
|
if (bytesRead == -1) {
|
||||||
return RESULT_END_OF_STREAM;
|
return RESULT_END_OF_STREAM;
|
||||||
}
|
}
|
||||||
|
|
@ -377,13 +350,10 @@ public final class FragmentedMp4Extractor {
|
||||||
atomType = atomHeader.readInt();
|
atomType = atomHeader.readInt();
|
||||||
|
|
||||||
if (atomType == Atom.TYPE_mdat) {
|
if (atomType == Atom.TYPE_mdat) {
|
||||||
int cencAuxSize = fragmentRun.auxiliarySampleInfoTotalSize;
|
if (fragmentRun.sampleEncryptionDataNeedsFill) {
|
||||||
if (cencAuxSize > 0) {
|
enterState(STATE_READING_ENCRYPTION_DATA);
|
||||||
cencAuxiliaryData = new ParsableByteArray(cencAuxSize);
|
|
||||||
enterState(STATE_READING_CENC_AUXILIARY_DATA);
|
|
||||||
} else {
|
} else {
|
||||||
cencAuxiliaryData = null;
|
enterState(STATE_READING_SAMPLE);
|
||||||
enterState(STATE_READING_SAMPLE_START);
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -395,7 +365,7 @@ public final class FragmentedMp4Extractor {
|
||||||
containerAtomEndPoints.add(rootAtomBytesRead + atomSize - ATOM_HEADER_SIZE);
|
containerAtomEndPoints.add(rootAtomBytesRead + atomSize - ATOM_HEADER_SIZE);
|
||||||
} else {
|
} else {
|
||||||
atomData = new ParsableByteArray(atomSize);
|
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);
|
enterState(STATE_READING_ATOM_PAYLOAD);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -409,7 +379,7 @@ public final class FragmentedMp4Extractor {
|
||||||
private int readAtomPayload(NonBlockingInputStream inputStream) {
|
private int readAtomPayload(NonBlockingInputStream inputStream) {
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
if (atomData != null) {
|
if (atomData != null) {
|
||||||
bytesRead = inputStream.read(atomData.getData(), atomBytesRead, atomSize - atomBytesRead);
|
bytesRead = inputStream.read(atomData.data, atomBytesRead, atomSize - atomBytesRead);
|
||||||
} else {
|
} else {
|
||||||
bytesRead = inputStream.skip(atomSize - atomBytesRead);
|
bytesRead = inputStream.skip(atomSize - atomBytesRead);
|
||||||
}
|
}
|
||||||
|
|
@ -441,8 +411,8 @@ public final class FragmentedMp4Extractor {
|
||||||
if (!containerAtoms.isEmpty()) {
|
if (!containerAtoms.isEmpty()) {
|
||||||
containerAtoms.peek().add(leaf);
|
containerAtoms.peek().add(leaf);
|
||||||
} else if (leaf.type == Atom.TYPE_sidx) {
|
} else if (leaf.type == Atom.TYPE_sidx) {
|
||||||
segmentIndex = parseSidx(leaf.getData());
|
segmentIndex = parseSidx(leaf.data);
|
||||||
return RESULT_READ_SIDX;
|
return RESULT_READ_INDEX;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -450,7 +420,7 @@ public final class FragmentedMp4Extractor {
|
||||||
private int onContainerAtomRead(ContainerAtom container) {
|
private int onContainerAtomRead(ContainerAtom container) {
|
||||||
if (container.type == Atom.TYPE_moov) {
|
if (container.type == Atom.TYPE_moov) {
|
||||||
onMoovContainerAtomRead(container);
|
onMoovContainerAtomRead(container);
|
||||||
return RESULT_READ_MOOV;
|
return RESULT_READ_INIT;
|
||||||
} else if (container.type == Atom.TYPE_moof) {
|
} else if (container.type == Atom.TYPE_moof) {
|
||||||
onMoofContainerAtomRead(container);
|
onMoofContainerAtomRead(container);
|
||||||
} else if (!containerAtoms.isEmpty()) {
|
} else if (!containerAtoms.isEmpty()) {
|
||||||
|
|
@ -460,11 +430,12 @@ public final class FragmentedMp4Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMoovContainerAtomRead(ContainerAtom moov) {
|
private void onMoovContainerAtomRead(ContainerAtom moov) {
|
||||||
List<Atom> moovChildren = moov.getChildren();
|
List<Atom> moovChildren = moov.children;
|
||||||
for (int i = 0; i < moovChildren.size(); i++) {
|
int moovChildrenSize = moovChildren.size();
|
||||||
|
for (int i = 0; i < moovChildrenSize; i++) {
|
||||||
Atom child = moovChildren.get(i);
|
Atom child = moovChildren.get(i);
|
||||||
if (child.type == Atom.TYPE_pssh) {
|
if (child.type == Atom.TYPE_pssh) {
|
||||||
ParsableByteArray psshAtom = ((LeafAtom) child).getData();
|
ParsableByteArray psshAtom = ((LeafAtom) child).data;
|
||||||
psshAtom.setPosition(FULL_ATOM_HEADER_SIZE);
|
psshAtom.setPosition(FULL_ATOM_HEADER_SIZE);
|
||||||
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
|
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
|
||||||
int dataSize = psshAtom.readInt();
|
int dataSize = psshAtom.readInt();
|
||||||
|
|
@ -474,13 +445,13 @@ public final class FragmentedMp4Extractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
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));
|
track = parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
||||||
fragmentRun = new TrackFragment();
|
fragmentRun.reset();
|
||||||
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags);
|
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
|
||||||
sampleIndex = 0;
|
sampleIndex = 0;
|
||||||
lastSyncSampleIndex = 0;
|
lastSyncSampleIndex = 0;
|
||||||
pendingSeekSyncSampleIndex = 0;
|
pendingSeekSyncSampleIndex = 0;
|
||||||
|
|
@ -514,21 +485,21 @@ public final class FragmentedMp4Extractor {
|
||||||
*/
|
*/
|
||||||
private static Track parseTrak(ContainerAtom trak) {
|
private static Track parseTrak(ContainerAtom trak) {
|
||||||
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
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);
|
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;
|
int id = header.first;
|
||||||
// TODO: This value should be used to set a duration field on the Track object
|
// 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
|
// 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).
|
// should set it anyway (and just have it be wrong for bad media streams).
|
||||||
// long duration = header.second;
|
// 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)
|
ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
|
||||||
.getContainerAtomOfType(Atom.TYPE_stbl);
|
.getContainerAtomOfType(Atom.TYPE_stbl);
|
||||||
|
|
||||||
Pair<MediaFormat, TrackEncryptionBox[]> sampleDescriptions =
|
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);
|
return new Track(id, trackType, timescale, sampleDescriptions.first, sampleDescriptions.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -654,7 +625,7 @@ public final class FragmentedMp4Extractor {
|
||||||
if (childAtomType == Atom.TYPE_esds) {
|
if (childAtomType == Atom.TYPE_esds) {
|
||||||
initializationData = parseEsdsFromParent(parent, childStartPosition);
|
initializationData = parseEsdsFromParent(parent, childStartPosition);
|
||||||
// TODO: Do we really need to do this? See [redacted]
|
// 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 =
|
Pair<Integer, Integer> audioSpecificConfig =
|
||||||
CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData);
|
CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData);
|
||||||
sampleRate = audioSpecificConfig.first;
|
sampleRate = audioSpecificConfig.first;
|
||||||
|
|
@ -697,7 +668,7 @@ public final class FragmentedMp4Extractor {
|
||||||
int length = atom.readUnsignedShort();
|
int length = atom.readUnsignedShort();
|
||||||
int offset = atom.getPosition();
|
int offset = atom.getPosition();
|
||||||
atom.skip(length);
|
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,
|
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
|
||||||
|
|
@ -775,7 +746,7 @@ public final class FragmentedMp4Extractor {
|
||||||
parent.skip(13);
|
parent.skip(13);
|
||||||
|
|
||||||
// Start of AudioSpecificConfig (defined in 14496-3)
|
// Start of AudioSpecificConfig (defined in 14496-3)
|
||||||
parent.skip(1); // AudioSpecificConfig tag
|
parent.skip(1); // AudioSpecificConfig tag
|
||||||
varIntByte = parent.readUnsignedByte();
|
varIntByte = parent.readUnsignedByte();
|
||||||
int varInt = varIntByte & 0x7F;
|
int varInt = varIntByte & 0x7F;
|
||||||
while (varIntByte > 127) {
|
while (varIntByte > 127) {
|
||||||
|
|
@ -789,49 +760,47 @@ public final class FragmentedMp4Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
|
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
|
||||||
ContainerAtom moof, TrackFragment out, int workaroundFlags) {
|
ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
|
||||||
// TODO: Consider checking that the sequence number returned by parseMfhd is as expected.
|
|
||||||
parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
|
|
||||||
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
|
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
|
||||||
out, workaroundFlags);
|
out, workaroundFlags, extendedTypeScratch);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a traf atom (defined in 14496-12).
|
* Parses a traf atom (defined in 14496-12).
|
||||||
*/
|
*/
|
||||||
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
|
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
|
||||||
ContainerAtom traf, TrackFragment out, int workaroundFlags) {
|
ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
|
||||||
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
|
||||||
if (saiz != null) {
|
|
||||||
parseSaiz(saiz.getData(), out);
|
|
||||||
}
|
|
||||||
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
|
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
|
||||||
long decodeTime = tfdtAtom == null ? 0
|
long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
|
||||||
: parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).getData());
|
|
||||||
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
|
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
|
||||||
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.getData());
|
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
|
||||||
out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
|
out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
|
||||||
|
|
||||||
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
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);
|
LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
|
||||||
if (uuid != null) {
|
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);
|
saiz.setPosition(ATOM_HEADER_SIZE);
|
||||||
int fullAtom = saiz.readInt();
|
int fullAtom = saiz.readInt();
|
||||||
int flags = parseFullAtomFlags(fullAtom);
|
int flags = parseFullAtomFlags(fullAtom);
|
||||||
|
|
@ -839,21 +808,26 @@ public final class FragmentedMp4Extractor {
|
||||||
saiz.skip(8);
|
saiz.skip(8);
|
||||||
}
|
}
|
||||||
int defaultSampleInfoSize = saiz.readUnsignedByte();
|
int defaultSampleInfoSize = saiz.readUnsignedByte();
|
||||||
|
|
||||||
int sampleCount = saiz.readUnsignedIntToInt();
|
int sampleCount = saiz.readUnsignedIntToInt();
|
||||||
|
if (sampleCount != out.length) {
|
||||||
|
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
|
||||||
|
}
|
||||||
|
|
||||||
int totalSize = 0;
|
int totalSize = 0;
|
||||||
int[] sampleInfoSizes = new int[sampleCount];
|
|
||||||
if (defaultSampleInfoSize == 0) {
|
if (defaultSampleInfoSize == 0) {
|
||||||
|
boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
|
||||||
for (int i = 0; i < sampleCount; i++) {
|
for (int i = 0; i < sampleCount; i++) {
|
||||||
sampleInfoSizes[i] = saiz.readUnsignedByte();
|
int sampleInfoSize = saiz.readUnsignedByte();
|
||||||
totalSize += sampleInfoSizes[i];
|
totalSize += sampleInfoSize;
|
||||||
|
sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < sampleCount; i++) {
|
boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
|
||||||
sampleInfoSizes[i] = defaultSampleInfoSize;
|
totalSize += defaultSampleInfoSize * sampleCount;
|
||||||
totalSize += defaultSampleInfoSize;
|
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) {
|
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
|
||||||
trun.setPosition(ATOM_HEADER_SIZE);
|
trun.setPosition(ATOM_HEADER_SIZE);
|
||||||
int fullAtom = trun.readInt();
|
int fullAtom = trun.readInt();
|
||||||
int version = parseFullAtomVersion(fullAtom);
|
|
||||||
int flags = parseFullAtomFlags(fullAtom);
|
int flags = parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
int numberOfEntries = trun.readUnsignedIntToInt();
|
int sampleCount = trun.readUnsignedIntToInt();
|
||||||
if ((flags & 0x01 /* data_offset_present */) != 0) {
|
if ((flags & 0x01 /* data_offset_present */) != 0) {
|
||||||
trun.skip(4);
|
trun.skip(4);
|
||||||
}
|
}
|
||||||
|
|
@ -932,17 +905,18 @@ public final class FragmentedMp4Extractor {
|
||||||
boolean sampleCompositionTimeOffsetsPresent =
|
boolean sampleCompositionTimeOffsetsPresent =
|
||||||
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
|
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
|
||||||
|
|
||||||
int[] sampleSizeTable = new int[numberOfEntries];
|
out.initTables(sampleCount);
|
||||||
int[] sampleDecodingTimeTable = new int[numberOfEntries];
|
int[] sampleSizeTable = out.sampleSizeTable;
|
||||||
int[] sampleCompositionTimeOffsetTable = new int[numberOfEntries];
|
int[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
|
||||||
boolean[] sampleIsSyncFrameTable = new boolean[numberOfEntries];
|
int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
|
||||||
|
boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
|
||||||
|
|
||||||
long timescale = track.timescale;
|
long timescale = track.timescale;
|
||||||
long cumulativeTime = decodeTime;
|
long cumulativeTime = decodeTime;
|
||||||
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
|
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
|
||||||
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
|
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
|
||||||
== 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.
|
// Use trun values if present, otherwise tfhd, otherwise trex.
|
||||||
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
|
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
|
||||||
: defaultSampleValues.duration;
|
: defaultSampleValues.duration;
|
||||||
|
|
@ -950,48 +924,47 @@ public final class FragmentedMp4Extractor {
|
||||||
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
|
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
|
||||||
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
|
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
|
||||||
if (sampleCompositionTimeOffsetsPresent) {
|
if (sampleCompositionTimeOffsetsPresent) {
|
||||||
int sampleOffset;
|
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
|
||||||
if (version == 0) {
|
// version 0 trun boxes, however a significant number of streams violate the spec and use
|
||||||
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
|
// signed integers instead. It's safe to always parse sample offsets as signed integers
|
||||||
// version 0 trun boxes, however a significant number of streams violate the spec and use
|
// here, because unsigned integers will still be parsed correctly (unless their top bit is
|
||||||
// signed integers instead. It's safe to always parse sample offsets as signed integers
|
// set, which is never true in practice because sample offsets are always small).
|
||||||
// here, because unsigned integers will still be parsed correctly (unless their top bit is
|
int sampleOffset = trun.readInt();
|
||||||
// set, which is never true in practice because sample offsets are always small).
|
|
||||||
sampleOffset = trun.readInt();
|
|
||||||
} else {
|
|
||||||
sampleOffset = trun.readInt();
|
|
||||||
}
|
|
||||||
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
|
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
|
||||||
|
} else {
|
||||||
|
sampleCompositionTimeOffsetTable[i] = 0;
|
||||||
}
|
}
|
||||||
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
|
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
|
||||||
sampleSizeTable[i] = sampleSize;
|
sampleSizeTable[i] = sampleSize;
|
||||||
boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
|
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
|
||||||
if (workaroundEveryVideoFrameIsSyncFrame && i != 0) {
|
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
|
||||||
isSync = false;
|
|
||||||
}
|
|
||||||
if (isSync) {
|
|
||||||
sampleIsSyncFrameTable[i] = true;
|
|
||||||
}
|
|
||||||
cumulativeTime += sampleDuration;
|
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);
|
uuid.setPosition(ATOM_HEADER_SIZE);
|
||||||
byte[] extendedType = new byte[16];
|
uuid.readBytes(extendedTypeScratch, 0, 16);
|
||||||
uuid.readBytes(extendedType, 0, 16);
|
|
||||||
|
|
||||||
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See "Portable encoding of audio-video objects: The Protected Interoperable File Format
|
// Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
|
||||||
// (PIFF), John A. Bocharov et al, Section 5.3.2.1."
|
// audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
|
||||||
int fullAtom = uuid.readInt();
|
// 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);
|
int flags = parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
|
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;
|
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
|
||||||
int numberOfEntries = uuid.readUnsignedIntToInt();
|
int sampleCount = senc.readUnsignedIntToInt();
|
||||||
if (numberOfEntries != out.length) {
|
if (sampleCount != out.length) {
|
||||||
throw new IllegalStateException("Length mismatch: " + numberOfEntries + ", " + out.length);
|
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
int sampleEncryptionDataLength = uuid.length() - uuid.getPosition();
|
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
|
||||||
ParsableByteArray sampleEncryptionData = new ParsableByteArray(sampleEncryptionDataLength);
|
out.initEncryptionData(senc.length() - senc.getPosition());
|
||||||
uuid.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionData.length());
|
out.fillEncryptionData(senc);
|
||||||
out.setSmoothStreamingSampleEncryptionData(sampleEncryptionData, subsampleEncryption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1067,18 +1039,12 @@ public final class FragmentedMp4Extractor {
|
||||||
return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs);
|
return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readCencAuxiliaryData(NonBlockingInputStream inputStream) {
|
private int readEncryptionData(NonBlockingInputStream inputStream) {
|
||||||
int length = cencAuxiliaryData.length();
|
boolean success = fragmentRun.fillEncryptionData(inputStream);
|
||||||
int bytesRead = inputStream.read(cencAuxiliaryData.getData(), cencAuxiliaryBytesRead,
|
if (!success) {
|
||||||
length - cencAuxiliaryBytesRead);
|
|
||||||
if (bytesRead == -1) {
|
|
||||||
return RESULT_END_OF_STREAM;
|
|
||||||
}
|
|
||||||
cencAuxiliaryBytesRead += bytesRead;
|
|
||||||
if (cencAuxiliaryBytesRead < length) {
|
|
||||||
return RESULT_NEED_MORE_DATA;
|
return RESULT_NEED_MORE_DATA;
|
||||||
}
|
}
|
||||||
enterState(STATE_READING_SAMPLE_START);
|
enterState(STATE_READING_SAMPLE);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1105,89 +1071,62 @@ public final class FragmentedMp4Extractor {
|
||||||
enterState(STATE_READING_ATOM_HEADER);
|
enterState(STATE_READING_ATOM_HEADER);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (sampleIndex < pendingSeekSyncSampleIndex) {
|
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
||||||
return skipSample(inputStream);
|
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) {
|
private int skipSample(NonBlockingInputStream inputStream, int sampleSize) {
|
||||||
if (parserState == STATE_READING_SAMPLE_START) {
|
if (fragmentRun.definesEncryptionData) {
|
||||||
ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
|
ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData;
|
||||||
: fragmentRun.smoothStreamingSampleEncryptionData;
|
TrackEncryptionBox encryptionBox =
|
||||||
if (sampleEncryptionData != null) {
|
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
|
||||||
TrackEncryptionBox encryptionBox =
|
int vectorSize = encryptionBox.initializationVectorSize;
|
||||||
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
|
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
|
||||||
int vectorSize = encryptionBox.initializationVectorSize;
|
sampleEncryptionData.skip(vectorSize);
|
||||||
boolean subsampleEncryption = cencAuxiliaryData != null
|
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
|
||||||
? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
|
if (subsampleEncryption) {
|
||||||
: fragmentRun.smoothStreamingUsesSubsampleEncryption;
|
sampleEncryptionData.skip((2 + 4) * subsampleCount);
|
||||||
sampleEncryptionData.skip(vectorSize);
|
|
||||||
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
|
|
||||||
if (subsampleEncryption) {
|
|
||||||
sampleEncryptionData.skip((2 + 4) * subsampleCount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
inputStream.skip(sampleSize);
|
||||||
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;
|
|
||||||
}
|
|
||||||
sampleIndex++;
|
sampleIndex++;
|
||||||
enterState(STATE_READING_SAMPLE_START);
|
enterState(STATE_READING_SAMPLE);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
|
private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) {
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
return RESULT_NEED_SAMPLE_HOLDER;
|
return RESULT_NEED_SAMPLE_HOLDER;
|
||||||
}
|
}
|
||||||
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
|
||||||
ByteBuffer outputData = out.data;
|
ByteBuffer outputData = out.data;
|
||||||
if (parserState == STATE_READING_SAMPLE_START) {
|
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
|
||||||
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
|
out.flags = 0;
|
||||||
out.flags = 0;
|
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
|
||||||
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
|
out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC;
|
||||||
out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC;
|
lastSyncSampleIndex = sampleIndex;
|
||||||
lastSyncSampleIndex = sampleIndex;
|
}
|
||||||
}
|
if (out.allowDataBufferReplacement && (out.data == null || out.data.capacity() < sampleSize)) {
|
||||||
if (out.allowDataBufferReplacement
|
outputData = ByteBuffer.allocate(sampleSize);
|
||||||
&& (out.data == null || out.data.capacity() < sampleSize)) {
|
out.data = outputData;
|
||||||
outputData = ByteBuffer.allocate(sampleSize);
|
}
|
||||||
out.data = outputData;
|
if (fragmentRun.definesEncryptionData) {
|
||||||
}
|
readSampleEncryptionData(fragmentRun.sampleEncryptionData, out);
|
||||||
ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
|
|
||||||
: fragmentRun.smoothStreamingSampleEncryptionData;
|
|
||||||
if (sampleEncryptionData != null) {
|
|
||||||
readSampleEncryptionData(sampleEncryptionData, out);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int bytesRead;
|
|
||||||
if (outputData == null) {
|
if (outputData == null) {
|
||||||
bytesRead = inputStream.skip(sampleSize - sampleBytesRead);
|
inputStream.skip(sampleSize);
|
||||||
|
out.size = 0;
|
||||||
} else {
|
} else {
|
||||||
bytesRead = inputStream.read(outputData, sampleSize - sampleBytesRead);
|
inputStream.read(outputData, sampleSize);
|
||||||
}
|
|
||||||
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) {
|
|
||||||
if (track.type == Track.TYPE_VIDEO) {
|
if (track.type == Track.TYPE_VIDEO) {
|
||||||
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code
|
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code
|
||||||
// delimited content. Replace length prefixes with start codes.
|
// delimited content. Replace length prefixes with start codes.
|
||||||
|
|
@ -1203,13 +1142,11 @@ public final class FragmentedMp4Extractor {
|
||||||
outputData.position(sampleOffset + sampleSize);
|
outputData.position(sampleOffset + sampleSize);
|
||||||
}
|
}
|
||||||
out.size = sampleSize;
|
out.size = sampleSize;
|
||||||
} else {
|
|
||||||
out.size = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleIndex++;
|
sampleIndex++;
|
||||||
enterState(STATE_READING_SAMPLE_START);
|
enterState(STATE_READING_SAMPLE);
|
||||||
return RESULT_READ_SAMPLE_FULL;
|
return RESULT_READ_SAMPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
|
|
@ -1219,9 +1156,7 @@ public final class FragmentedMp4Extractor {
|
||||||
byte[] keyId = encryptionBox.keyId;
|
byte[] keyId = encryptionBox.keyId;
|
||||||
boolean isEncrypted = encryptionBox.isEncrypted;
|
boolean isEncrypted = encryptionBox.isEncrypted;
|
||||||
int vectorSize = encryptionBox.initializationVectorSize;
|
int vectorSize = encryptionBox.initializationVectorSize;
|
||||||
boolean subsampleEncryption = cencAuxiliaryData != null
|
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
|
||||||
? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
|
|
||||||
: fragmentRun.smoothStreamingUsesSubsampleEncryption;
|
|
||||||
|
|
||||||
byte[] vector = out.cryptoInfo.iv;
|
byte[] vector = out.cryptoInfo.iv;
|
||||||
if (vector == null || vector.length != 16) {
|
if (vector == null || vector.length != 16) {
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,14 @@ import java.nio.ByteBuffer;
|
||||||
*/
|
*/
|
||||||
/* package */ final class ParsableByteArray {
|
/* package */ final class ParsableByteArray {
|
||||||
|
|
||||||
private final byte[] data;
|
public byte[] data;
|
||||||
|
|
||||||
private int position;
|
private int position;
|
||||||
|
|
||||||
public ParsableByteArray(int length) {
|
public ParsableByteArray(int length) {
|
||||||
this.data = new byte[length];
|
this.data = new byte[length];
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int length() {
|
public int length() {
|
||||||
return data.length;
|
return data.length;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer.parser.mp4;
|
||||||
/**
|
/**
|
||||||
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
|
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
|
||||||
*/
|
*/
|
||||||
public class TrackEncryptionBox {
|
public final class TrackEncryptionBox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates the encryption state of the samples in the sample group.
|
* Indicates the encryption state of the samples in the sample group.
|
||||||
|
|
|
||||||
|
|
@ -15,48 +15,136 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.parser.mp4;
|
package com.google.android.exoplayer.parser.mp4;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A holder for information corresponding to a single fragment of an mp4 file.
|
* A holder for information corresponding to a single fragment of an mp4 file.
|
||||||
*/
|
*/
|
||||||
/* package */ class TrackFragment {
|
/* package */ final class TrackFragment {
|
||||||
|
|
||||||
public int sampleDescriptionIndex;
|
public int sampleDescriptionIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of samples contained by the fragment.
|
||||||
|
*/
|
||||||
public int length;
|
public int length;
|
||||||
|
/**
|
||||||
|
* The size of each sample in the run.
|
||||||
|
*/
|
||||||
public int[] sampleSizeTable;
|
public int[] sampleSizeTable;
|
||||||
|
/**
|
||||||
|
* The decoding time of each sample in the run.
|
||||||
|
*/
|
||||||
public int[] sampleDecodingTimeTable;
|
public int[] sampleDecodingTimeTable;
|
||||||
|
/**
|
||||||
|
* The composition time offset of each sample in the run.
|
||||||
|
*/
|
||||||
public int[] sampleCompositionTimeOffsetTable;
|
public int[] sampleCompositionTimeOffsetTable;
|
||||||
|
/**
|
||||||
|
* Indicates which samples are sync frames.
|
||||||
|
*/
|
||||||
public boolean[] sampleIsSyncFrameTable;
|
public boolean[] sampleIsSyncFrameTable;
|
||||||
|
/**
|
||||||
|
* True if the fragment defines encryption data. False otherwise.
|
||||||
|
*/
|
||||||
|
public boolean definesEncryptionData;
|
||||||
|
/**
|
||||||
|
* If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.
|
||||||
|
* Undefined otherwise.
|
||||||
|
*/
|
||||||
|
public boolean[] sampleHasSubsampleEncryptionTable;
|
||||||
|
/**
|
||||||
|
* If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.
|
||||||
|
* Undefined otherwise.
|
||||||
|
*/
|
||||||
|
public int sampleEncryptionDataLength;
|
||||||
|
/**
|
||||||
|
* If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public ParsableByteArray sampleEncryptionData;
|
||||||
|
/**
|
||||||
|
* Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.
|
||||||
|
*/
|
||||||
|
public boolean sampleEncryptionDataNeedsFill;
|
||||||
|
|
||||||
public int auxiliarySampleInfoTotalSize;
|
/**
|
||||||
public int[] auxiliarySampleInfoSizeTable;
|
* Resets the fragment.
|
||||||
|
* <p>
|
||||||
public boolean smoothStreamingUsesSubsampleEncryption;
|
* The {@link #length} is set to 0, and both {@link #definesEncryptionData} and
|
||||||
public ParsableByteArray smoothStreamingSampleEncryptionData;
|
* {@link #sampleEncryptionDataNeedsFill} is set to false.
|
||||||
|
*/
|
||||||
public void setSampleDescriptionIndex(int sampleDescriptionIndex) {
|
public void reset() {
|
||||||
this.sampleDescriptionIndex = sampleDescriptionIndex;
|
length = 0;
|
||||||
|
definesEncryptionData = false;
|
||||||
|
sampleEncryptionDataNeedsFill = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSampleTables(int[] sampleSizeTable, int[] sampleDecodingTimeTable,
|
/**
|
||||||
int[] sampleCompositionTimeOffsetTable, boolean[] sampleIsSyncFrameTable) {
|
* Configures the fragment for the specified number of samples.
|
||||||
this.sampleSizeTable = sampleSizeTable;
|
* <p>
|
||||||
this.sampleDecodingTimeTable = sampleDecodingTimeTable;
|
* The {@link #length} of the fragment is set to the specified sample count, and the contained
|
||||||
this.sampleCompositionTimeOffsetTable = sampleCompositionTimeOffsetTable;
|
* tables are resized if necessary such that they are at least this length.
|
||||||
this.sampleIsSyncFrameTable = sampleIsSyncFrameTable;
|
*
|
||||||
this.length = sampleSizeTable.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) {
|
* Configures the fragment to be one that defines encryption data of the specified length.
|
||||||
this.auxiliarySampleInfoTotalSize = totalAuxiliarySampleInfoSize;
|
* <p>
|
||||||
this.auxiliarySampleInfoSizeTable = auxiliarySampleInfoSizeTable;
|
* {@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) {
|
* Fills {@link #sampleEncryptionData} from the provided source.
|
||||||
this.smoothStreamingSampleEncryptionData = data;
|
*
|
||||||
this.smoothStreamingUsesSubsampleEncryption = usesSubsampleEncryption;
|
* @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) {
|
public int getSamplePresentationTime(int index) {
|
||||||
|
|
|
||||||
|
|
@ -138,9 +138,8 @@ import java.util.Stack;
|
||||||
while (true) {
|
while (true) {
|
||||||
while (!masterElementsStack.isEmpty()
|
while (!masterElementsStack.isEmpty()
|
||||||
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
|
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
|
||||||
if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) {
|
eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId);
|
||||||
return READ_RESULT_CONTINUE;
|
return READ_RESULT_CONTINUE;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == STATE_BEGIN_READING) {
|
if (state == STATE_BEGIN_READING) {
|
||||||
|
|
@ -161,12 +160,10 @@ import java.util.Stack;
|
||||||
case TYPE_MASTER:
|
case TYPE_MASTER:
|
||||||
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
|
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
|
||||||
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
|
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
|
||||||
if (!eventHandler.onMasterElementStart(
|
eventHandler.onMasterElementStart(elementId, elementOffset, masterHeaderSize,
|
||||||
elementId, elementOffset, masterHeaderSize, elementContentSize)) {
|
elementContentSize);
|
||||||
prepareForNextElement();
|
prepareForNextElement();
|
||||||
return READ_RESULT_CONTINUE;
|
return READ_RESULT_CONTINUE;
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TYPE_UNSIGNED_INT:
|
case TYPE_UNSIGNED_INT:
|
||||||
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
|
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
|
||||||
throw new IllegalStateException("Invalid integer size " + elementContentSize);
|
throw new IllegalStateException("Invalid integer size " + elementContentSize);
|
||||||
|
|
@ -177,11 +174,9 @@ import java.util.Stack;
|
||||||
return intResult;
|
return intResult;
|
||||||
}
|
}
|
||||||
long intValue = getTempByteArrayValue((int) elementContentSize, false);
|
long intValue = getTempByteArrayValue((int) elementContentSize, false);
|
||||||
if (!eventHandler.onIntegerElement(elementId, intValue)) {
|
eventHandler.onIntegerElement(elementId, intValue);
|
||||||
prepareForNextElement();
|
prepareForNextElement();
|
||||||
return READ_RESULT_CONTINUE;
|
return READ_RESULT_CONTINUE;
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TYPE_FLOAT:
|
case TYPE_FLOAT:
|
||||||
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
|
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
|
||||||
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
|
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
|
||||||
|
|
@ -199,11 +194,9 @@ import java.util.Stack;
|
||||||
} else {
|
} else {
|
||||||
floatValue = Double.longBitsToDouble(valueBits);
|
floatValue = Double.longBitsToDouble(valueBits);
|
||||||
}
|
}
|
||||||
if (!eventHandler.onFloatElement(elementId, floatValue)) {
|
eventHandler.onFloatElement(elementId, floatValue);
|
||||||
prepareForNextElement();
|
prepareForNextElement();
|
||||||
return READ_RESULT_CONTINUE;
|
return READ_RESULT_CONTINUE;
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TYPE_STRING:
|
case TYPE_STRING:
|
||||||
if (elementContentSize > Integer.MAX_VALUE) {
|
if (elementContentSize > Integer.MAX_VALUE) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
|
|
@ -219,11 +212,9 @@ import java.util.Stack;
|
||||||
}
|
}
|
||||||
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
|
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
|
||||||
stringBytes = null;
|
stringBytes = null;
|
||||||
if (!eventHandler.onStringElement(elementId, stringValue)) {
|
eventHandler.onStringElement(elementId, stringValue);
|
||||||
prepareForNextElement();
|
prepareForNextElement();
|
||||||
return READ_RESULT_CONTINUE;
|
return READ_RESULT_CONTINUE;
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TYPE_BINARY:
|
case TYPE_BINARY:
|
||||||
if (elementContentSize > Integer.MAX_VALUE) {
|
if (elementContentSize > Integer.MAX_VALUE) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
|
|
@ -233,18 +224,17 @@ import java.util.Stack;
|
||||||
return READ_RESULT_NEED_MORE_DATA;
|
return READ_RESULT_NEED_MORE_DATA;
|
||||||
}
|
}
|
||||||
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
|
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
|
||||||
boolean keepGoing = eventHandler.onBinaryElement(
|
boolean consumed = eventHandler.onBinaryElement(
|
||||||
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
|
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
|
||||||
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
|
if (consumed) {
|
||||||
if (expectedBytesRead != bytesRead) {
|
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
|
||||||
throw new IllegalStateException("Incorrect total bytes read. Expected "
|
if (expectedBytesRead != bytesRead) {
|
||||||
+ expectedBytesRead + " but actually " + bytesRead);
|
throw new IllegalStateException("Incorrect total bytes read. Expected "
|
||||||
}
|
+ expectedBytesRead + " but actually " + bytesRead);
|
||||||
if (!keepGoing) {
|
}
|
||||||
prepareForNextElement();
|
prepareForNextElement();
|
||||||
return READ_RESULT_CONTINUE;
|
|
||||||
}
|
}
|
||||||
break;
|
return READ_RESULT_CONTINUE;
|
||||||
case TYPE_UNKNOWN:
|
case TYPE_UNKNOWN:
|
||||||
if (elementContentSize > Integer.MAX_VALUE) {
|
if (elementContentSize > Integer.MAX_VALUE) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
|
|
@ -254,11 +244,11 @@ import java.util.Stack;
|
||||||
if (skipResult != READ_RESULT_CONTINUE) {
|
if (skipResult != READ_RESULT_CONTINUE) {
|
||||||
return skipResult;
|
return skipResult;
|
||||||
}
|
}
|
||||||
|
prepareForNextElement();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Invalid element type " + type);
|
throw new IllegalStateException("Invalid element type " + type);
|
||||||
}
|
}
|
||||||
prepareForNextElement();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,7 +498,7 @@ import java.util.Stack;
|
||||||
*/
|
*/
|
||||||
private int updateBytesState(int additionalBytesRead, int totalBytes) {
|
private int updateBytesState(int additionalBytesRead, int totalBytes) {
|
||||||
if (additionalBytesRead == -1) {
|
if (additionalBytesRead == -1) {
|
||||||
return READ_RESULT_END_OF_FILE;
|
return READ_RESULT_END_OF_STREAM;
|
||||||
}
|
}
|
||||||
bytesState += additionalBytesRead;
|
bytesState += additionalBytesRead;
|
||||||
bytesRead += additionalBytesRead;
|
bytesRead += additionalBytesRead;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.MediaExtractor;
|
import android.media.MediaExtractor;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.TimeUnit;
|
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_FIXED = 2;
|
||||||
private static final int LACING_EBML = 3;
|
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 EbmlReader reader;
|
||||||
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
|
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
|
||||||
|
|
||||||
private SampleHolder tempSampleHolder;
|
private SampleHolder sampleHolder;
|
||||||
private boolean sampleRead;
|
private int readResults;
|
||||||
|
|
||||||
private boolean prepared = false;
|
|
||||||
private long segmentStartOffsetBytes = UNKNOWN;
|
private long segmentStartOffsetBytes = UNKNOWN;
|
||||||
private long segmentEndOffsetBytes = UNKNOWN;
|
private long segmentEndOffsetBytes = UNKNOWN;
|
||||||
private long timecodeScale = 1000000L;
|
private long timecodeScale = 1000000L;
|
||||||
|
|
@ -105,28 +108,29 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
/* package */ DefaultWebmExtractor(EbmlReader reader) {
|
/* package */ DefaultWebmExtractor(EbmlReader reader) {
|
||||||
this.reader = reader;
|
this.reader = reader;
|
||||||
this.reader.setEventHandler(new InnerEbmlEventHandler());
|
this.reader.setEventHandler(new InnerEbmlEventHandler());
|
||||||
this.cueTimesUs = new LongArray();
|
|
||||||
this.cueClusterPositions = new LongArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPrepared() {
|
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
|
||||||
return prepared;
|
this.sampleHolder = sampleHolder;
|
||||||
}
|
this.readResults = 0;
|
||||||
|
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
|
||||||
@Override
|
int ebmlReadResult = reader.read(inputStream);
|
||||||
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
|
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
|
||||||
tempSampleHolder = sampleHolder;
|
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
|
||||||
sampleRead = false;
|
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
|
||||||
reader.read(inputStream);
|
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
|
||||||
tempSampleHolder = null;
|
}
|
||||||
return sampleRead;
|
}
|
||||||
|
this.sampleHolder = null;
|
||||||
|
return readResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
||||||
checkPrepared();
|
|
||||||
if (allowNoop
|
if (allowNoop
|
||||||
|
&& cues != null
|
||||||
|
&& clusterTimecodeUs != UNKNOWN
|
||||||
&& simpleBlockTimecodeUs != UNKNOWN
|
&& simpleBlockTimecodeUs != UNKNOWN
|
||||||
&& seekTimeUs >= simpleBlockTimecodeUs) {
|
&& seekTimeUs >= simpleBlockTimecodeUs) {
|
||||||
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
|
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
|
||||||
|
|
@ -134,19 +138,19 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clusterTimecodeUs = UNKNOWN;
|
||||||
|
simpleBlockTimecodeUs = UNKNOWN;
|
||||||
reader.reset();
|
reader.reset();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SegmentIndex getCues() {
|
public SegmentIndex getIndex() {
|
||||||
checkPrepared();
|
|
||||||
return cues;
|
return cues;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaFormat getFormat() {
|
public MediaFormat getFormat() {
|
||||||
checkPrepared();
|
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,6 +200,8 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
break;
|
break;
|
||||||
case ID_CUES:
|
case ID_CUES:
|
||||||
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
|
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
|
||||||
|
cueTimesUs = new LongArray();
|
||||||
|
cueClusterPositions = new LongArray();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// pass
|
// pass
|
||||||
|
|
@ -204,11 +210,16 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ boolean onMasterElementEnd(int id) {
|
/* package */ boolean onMasterElementEnd(int id) {
|
||||||
if (id == ID_CUES) {
|
switch (id) {
|
||||||
finishPreparing();
|
case ID_CUES:
|
||||||
return false;
|
buildCues();
|
||||||
|
return false;
|
||||||
|
case ID_VIDEO:
|
||||||
|
buildFormat();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ boolean onIntegerElement(int id, long value) {
|
/* 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
|
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
|
||||||
// for info about how data is organized in a SimpleBlock element.
|
// 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.
|
// Value of trackNumber is not used but needs to be read.
|
||||||
reader.readVarint(inputStream);
|
reader.readVarint(inputStream);
|
||||||
|
|
||||||
|
|
@ -304,10 +321,10 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
case LACING_NONE:
|
case LACING_NONE:
|
||||||
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
|
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
|
||||||
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
|
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
|
||||||
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
||||||
tempSampleHolder.decodeOnly = invisible;
|
sampleHolder.decodeOnly = invisible;
|
||||||
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
|
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
|
||||||
tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
|
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
|
||||||
break;
|
break;
|
||||||
case LACING_EBML:
|
case LACING_EBML:
|
||||||
case LACING_FIXED:
|
case LACING_FIXED:
|
||||||
|
|
@ -316,44 +333,63 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
|
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read video data into sample holder.
|
ByteBuffer outputData = sampleHolder.data;
|
||||||
reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
|
if (sampleHolder.allowDataBufferReplacement
|
||||||
sampleRead = true;
|
&& (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
|
||||||
return false;
|
outputData = ByteBuffer.allocate(sampleHolder.size);
|
||||||
} else {
|
sampleHolder.data = outputData;
|
||||||
reader.skipBytes(inputStream, contentsSizeBytes);
|
}
|
||||||
return true;
|
|
||||||
|
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) {
|
private long scaleTimecodeToUs(long unscaledTimecode) {
|
||||||
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
|
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPrepared() {
|
/**
|
||||||
if (!prepared) {
|
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
|
||||||
throw new IllegalStateException("Parser not yet prepared");
|
*
|
||||||
|
* <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) {
|
* Build a {@link SegmentIndex} containing recently gathered Cues information.
|
||||||
throw new IllegalStateException("Already prepared");
|
*
|
||||||
} else if (segmentStartOffsetBytes == UNKNOWN) {
|
* <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");
|
throw new IllegalStateException("Segment start/end offsets unknown");
|
||||||
} else if (durationUs == UNKNOWN) {
|
} else if (durationUs == UNKNOWN) {
|
||||||
throw new IllegalStateException("Duration unknown");
|
throw new IllegalStateException("Duration unknown");
|
||||||
} else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
|
|
||||||
throw new IllegalStateException("Pixel width/height unknown");
|
|
||||||
} else if (cuesSizeBytes == UNKNOWN) {
|
} else if (cuesSizeBytes == UNKNOWN) {
|
||||||
throw new IllegalStateException("Cues size 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");
|
throw new IllegalStateException("Invalid/missing cue points");
|
||||||
}
|
}
|
||||||
|
|
||||||
format = MediaFormat.createVideoFormat(
|
|
||||||
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
|
|
||||||
|
|
||||||
int cuePointsSize = cueTimesUs.size();
|
int cuePointsSize = cueTimesUs.size();
|
||||||
int[] sizes = new int[cuePointsSize];
|
int[] sizes = new int[cuePointsSize];
|
||||||
long[] offsets = new long[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);
|
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
|
||||||
cueTimesUs = null;
|
cueTimesUs = null;
|
||||||
cueClusterPositions = null;
|
cueClusterPositions = null;
|
||||||
|
readResults |= RESULT_READ_INDEX;
|
||||||
prepared = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -388,30 +423,30 @@ public final class DefaultWebmExtractor implements WebmExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMasterElementStart(
|
public void onMasterElementStart(
|
||||||
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
||||||
return DefaultWebmExtractor.this.onMasterElementStart(
|
DefaultWebmExtractor.this.onMasterElementStart(
|
||||||
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
|
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMasterElementEnd(int id) {
|
public void onMasterElementEnd(int id) {
|
||||||
return DefaultWebmExtractor.this.onMasterElementEnd(id);
|
DefaultWebmExtractor.this.onMasterElementEnd(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onIntegerElement(int id, long value) {
|
public void onIntegerElement(int id, long value) {
|
||||||
return DefaultWebmExtractor.this.onIntegerElement(id, value);
|
DefaultWebmExtractor.this.onIntegerElement(id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onFloatElement(int id, double value) {
|
public void onFloatElement(int id, double value) {
|
||||||
return DefaultWebmExtractor.this.onFloatElement(id, value);
|
DefaultWebmExtractor.this.onFloatElement(id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onStringElement(int id, String value) {
|
public void onStringElement(int id, String value) {
|
||||||
return DefaultWebmExtractor.this.onStringElement(id, value);
|
DefaultWebmExtractor.this.onStringElement(id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,8 @@ import java.nio.ByteBuffer;
|
||||||
* @param elementOffsetBytes The byte offset where this element starts
|
* @param elementOffsetBytes The byte offset where this element starts
|
||||||
* @param headerSizeBytes The byte length of this element's ID and size header
|
* @param headerSizeBytes The byte length of this element's ID and size header
|
||||||
* @param contentsSizeBytes The byte length of this element's children
|
* @param contentsSizeBytes The byte length of this element's children
|
||||||
* @return {@code false} if parsing should stop right away
|
|
||||||
*/
|
*/
|
||||||
public boolean onMasterElementStart(
|
public void onMasterElementStart(
|
||||||
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
|
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,44 +55,42 @@ import java.nio.ByteBuffer;
|
||||||
* {@link NonBlockingInputStream}.
|
* {@link NonBlockingInputStream}.
|
||||||
*
|
*
|
||||||
* @param id The integer ID of this element
|
* @param id The integer ID of this element
|
||||||
* @return {@code false} if parsing should stop right away
|
|
||||||
*/
|
*/
|
||||||
public boolean onMasterElementEnd(int id);
|
public void onMasterElementEnd(int id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
|
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
|
||||||
*
|
*
|
||||||
* @param id The integer ID of this element
|
* @param id The integer ID of this element
|
||||||
* @param value The integer value this element contains
|
* @param value The integer value this element contains
|
||||||
* @return {@code false} if parsing should stop right away
|
|
||||||
*/
|
*/
|
||||||
public boolean onIntegerElement(int id, long value);
|
public void onIntegerElement(int id, long value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
|
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
|
||||||
*
|
*
|
||||||
* @param id The integer ID of this element
|
* @param id The integer ID of this element
|
||||||
* @param value The float value this element contains
|
* @param value The float value this element contains
|
||||||
* @return {@code false} if parsing should stop right away
|
|
||||||
*/
|
*/
|
||||||
public boolean onFloatElement(int id, double value);
|
public void onFloatElement(int id, double value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
|
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
|
||||||
*
|
*
|
||||||
* @param id The integer ID of this element
|
* @param id The integer ID of this element
|
||||||
* @param value The string value this element contains
|
* @param value The string value this element contains
|
||||||
* @return {@code false} if parsing should stop right away
|
|
||||||
*/
|
*/
|
||||||
public boolean onStringElement(int id, String value);
|
public void onStringElement(int id, String value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
|
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
|
||||||
*
|
*
|
||||||
* <p>The element header (containing element ID and content size) will already have been read.
|
* <p>The element header (containing element ID and content size) will already have been read.
|
||||||
* Subclasses must exactly read the entire contents of the element, which is
|
* Subclasses must either read nothing and return {@code false}, or exactly read the entire
|
||||||
* {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be
|
* contents of the element, which is {@code contentsSizeBytes} in length, and return {@code true}.
|
||||||
* immediately available from {@code inputStream}.
|
*
|
||||||
|
* <p>It's guaranteed that the full element contents will be immediately available from
|
||||||
|
* {@code inputStream}.
|
||||||
*
|
*
|
||||||
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a
|
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a
|
||||||
* binary element:
|
* binary element:
|
||||||
|
|
@ -111,7 +108,7 @@ import java.nio.ByteBuffer;
|
||||||
* @param contentsSizeBytes The byte length of this element's contents
|
* @param contentsSizeBytes The byte length of this element's contents
|
||||||
* @param inputStream The {@link NonBlockingInputStream} from which this
|
* @param inputStream The {@link NonBlockingInputStream} from which this
|
||||||
* element's contents should be read
|
* element's contents should be read
|
||||||
* @return {@code false} if parsing should stop right away
|
* @return True if the element was read. False otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean onBinaryElement(
|
public boolean onBinaryElement(
|
||||||
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,12 @@ import java.nio.ByteBuffer;
|
||||||
// Return values for reading methods.
|
// Return values for reading methods.
|
||||||
public static final int READ_RESULT_CONTINUE = 0;
|
public static final int READ_RESULT_CONTINUE = 0;
|
||||||
public static final int READ_RESULT_NEED_MORE_DATA = 1;
|
public static final int READ_RESULT_NEED_MORE_DATA = 1;
|
||||||
public static final int READ_RESULT_END_OF_FILE = 2;
|
public static final int READ_RESULT_END_OF_STREAM = 2;
|
||||||
|
|
||||||
public void setEventHandler(EbmlEventHandler eventHandler);
|
public void setEventHandler(EbmlEventHandler eventHandler);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
|
* Reads from a {@link NonBlockingInputStream}, invoking an event callback if possible.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read
|
* @param inputStream The input stream from which data should be read
|
||||||
* @return One of the {@code RESULT_*} flags defined in this interface
|
* @return One of the {@code RESULT_*} flags defined in this interface
|
||||||
|
|
|
||||||
|
|
@ -30,24 +30,38 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
public interface WebmExtractor {
|
public interface WebmExtractor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the has parsed the cues and sample format from the stream.
|
* An attempt to read from the input stream returned insufficient data.
|
||||||
*
|
|
||||||
* @return True if the extractor is prepared. False otherwise
|
|
||||||
*/
|
*/
|
||||||
public 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}.
|
* Consumes data from a {@link NonBlockingInputStream}.
|
||||||
*
|
*
|
||||||
* <p>If the return value is {@code false}, then a sample may have been partially read into
|
|
||||||
* {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
|
|
||||||
* in subsequent calls until the whole sample has been read.
|
|
||||||
*
|
|
||||||
* @param inputStream The input stream from which data should be read
|
* @param inputStream The input stream from which data should be read
|
||||||
* @param sampleHolder A {@link SampleHolder} into which the sample should be read
|
* @param sampleHolder A {@link SampleHolder} into which the sample should be read
|
||||||
* @return {@code true} if a sample has been read into the sample holder
|
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
||||||
*/
|
*/
|
||||||
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
|
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to a position before or equal to the requested time.
|
* Seeks to a position before or equal to the requested time.
|
||||||
|
|
@ -66,7 +80,7 @@ public interface WebmExtractor {
|
||||||
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
|
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
|
||||||
* prepared
|
* prepared
|
||||||
*/
|
*/
|
||||||
public SegmentIndex getCues();
|
public SegmentIndex getIndex();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the format of the samples contained within the media stream.
|
* Returns the format of the samples contained within the media stream.
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||||
source.continueBuffering(timeUs);
|
try {
|
||||||
|
source.continueBuffering(timeUs);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ExoPlaybackException(e);
|
||||||
|
}
|
||||||
|
|
||||||
currentPositionUs = timeUs;
|
currentPositionUs = timeUs;
|
||||||
|
|
||||||
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance
|
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance
|
||||||
|
|
@ -225,7 +230,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||||
@Override
|
@Override
|
||||||
protected long getBufferedPositionUs() {
|
protected long getBufferedPositionUs() {
|
||||||
// Don't block playback whilst subtitles are loading.
|
// Don't block playback whilst subtitles are loading.
|
||||||
return END_OF_TRACK;
|
return END_OF_TRACK_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -275,7 +280,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,28 @@ package com.google.android.exoplayer.upstream;
|
||||||
*/
|
*/
|
||||||
public interface Allocation {
|
public interface Allocation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the allocation has a capacity greater than or equal to the specified size in bytes.
|
||||||
|
* <p>
|
||||||
|
* If {@code size} is greater than the current capacity of the allocation, then it will grow
|
||||||
|
* to have a capacity of at least {@code size}. The allocation is grown by adding new fragments.
|
||||||
|
* Existing fragments remain unchanged, and any data that has been written to them will be
|
||||||
|
* preserved.
|
||||||
|
* <p>
|
||||||
|
* If {@code size} is less than or equal to the capacity of the allocation, then the call is a
|
||||||
|
* no-op.
|
||||||
|
*
|
||||||
|
* @param size The minimum required capacity, in bytes.
|
||||||
|
*/
|
||||||
|
public void ensureCapacity(int size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the capacity of the allocation, in bytes.
|
||||||
|
*
|
||||||
|
* @return The capacity of the allocation, in bytes.
|
||||||
|
*/
|
||||||
|
public int capacity();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the buffers in which the fragments are allocated.
|
* Gets the buffers in which the fragments are allocated.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ public interface BandwidthMeter {
|
||||||
final long NO_ESTIMATE = -1;
|
final long NO_ESTIMATE = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the estimated bandwidth.
|
* Gets the estimated bandwidth, in bits/sec.
|
||||||
*
|
*
|
||||||
* @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available.
|
* @return Estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if no estimate is available.
|
||||||
*/
|
*/
|
||||||
long getEstimate();
|
long getBitrateEstimate();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,15 +67,39 @@ public final class BufferPool implements Allocator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized Allocation allocate(int size) {
|
public synchronized Allocation allocate(int size) {
|
||||||
|
return new AllocationImpl(allocate(size, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocates byte arrays whose combined length is at least {@code size}.
|
||||||
|
* <p>
|
||||||
|
* An existing array of byte arrays may be provided to form the start of the allocation.
|
||||||
|
*
|
||||||
|
* @param size The total size required, in bytes.
|
||||||
|
* @param existing Existing byte arrays to use as the start of the allocation. May be null.
|
||||||
|
* @return The allocated byte arrays.
|
||||||
|
*/
|
||||||
|
/* package */ synchronized byte[][] allocate(int size, byte[][] existing) {
|
||||||
int requiredBufferCount = requiredBufferCount(size);
|
int requiredBufferCount = requiredBufferCount(size);
|
||||||
allocatedBufferCount += requiredBufferCount;
|
if (existing != null && requiredBufferCount <= existing.length) {
|
||||||
|
// The existing buffers are sufficient.
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
// We need to allocate additional buffers.
|
||||||
byte[][] buffers = new byte[requiredBufferCount][];
|
byte[][] buffers = new byte[requiredBufferCount][];
|
||||||
for (int i = 0; i < requiredBufferCount; i++) {
|
int firstNewBufferIndex = 0;
|
||||||
|
if (existing != null) {
|
||||||
|
firstNewBufferIndex = existing.length;
|
||||||
|
System.arraycopy(existing, 0, buffers, 0, firstNewBufferIndex);
|
||||||
|
}
|
||||||
|
// Allocate the new buffers
|
||||||
|
allocatedBufferCount += requiredBufferCount - firstNewBufferIndex;
|
||||||
|
for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) {
|
||||||
// Use a recycled buffer if one is available. Else instantiate a new one.
|
// Use a recycled buffer if one is available. Else instantiate a new one.
|
||||||
buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] :
|
buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] :
|
||||||
new byte[bufferLength];
|
new byte[bufferLength];
|
||||||
}
|
}
|
||||||
return new AllocationImpl(buffers);
|
return buffers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,6 +136,16 @@ public final class BufferPool implements Allocator {
|
||||||
this.buffers = buffers;
|
this.buffers = buffers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void ensureCapacity(int size) {
|
||||||
|
buffers = allocate(size, buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int capacity() {
|
||||||
|
return bufferLength * buffers.length;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[][] getBuffers() {
|
public byte[][] getBuffers() {
|
||||||
return buffers;
|
return buffers;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
@ -29,7 +30,7 @@ public class ByteArrayDataSink implements DataSink {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DataSink open(DataSpec dataSpec) throws IOException {
|
public DataSink open(DataSpec dataSpec) throws IOException {
|
||||||
if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) {
|
if (dataSpec.length == C.LENGTH_UNBOUNDED) {
|
||||||
stream = new ByteArrayOutputStream();
|
stream = new ByteArrayOutputStream();
|
||||||
} else {
|
} else {
|
||||||
Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
|
Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -36,14 +37,14 @@ public class ByteArrayDataSource implements DataSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long open(DataSpec dataSpec) throws IOException {
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) {
|
if (dataSpec.length == C.LENGTH_UNBOUNDED) {
|
||||||
Assertions.checkArgument(dataSpec.position < data.length);
|
Assertions.checkArgument(dataSpec.position < data.length);
|
||||||
} else {
|
} else {
|
||||||
Assertions.checkArgument(dataSpec.position + dataSpec.length <= data.length);
|
Assertions.checkArgument(dataSpec.position + dataSpec.length <= data.length);
|
||||||
}
|
}
|
||||||
readPosition = (int) dataSpec.position;
|
readPosition = (int) dataSpec.position;
|
||||||
return (dataSpec.length == DataSpec.LENGTH_UNBOUNDED)
|
return (dataSpec.length == C.LENGTH_UNBOUNDED) ? (data.length - dataSpec.position)
|
||||||
? (data.length - dataSpec.position) : dataSpec.length;
|
: dataSpec.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,9 +36,10 @@ public interface DataSource {
|
||||||
* @param dataSpec Defines the data to be read.
|
* @param dataSpec Defines the data to be read.
|
||||||
* @throws IOException If an error occurs opening the source.
|
* @throws IOException If an error occurs opening the source.
|
||||||
* @return The number of bytes that can be read from the opened source. For unbounded requests
|
* @return The number of bytes that can be read from the opened source. For unbounded requests
|
||||||
* (i.e. requests where {@link DataSpec#length} equals {@link DataSpec#LENGTH_UNBOUNDED})
|
* (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNBOUNDED}) this value
|
||||||
* this value is the resolved length of the request. For all other requests, the value
|
* is the resolved length of the request, or {@link C#LENGTH_UNBOUNDED} if the length is still
|
||||||
* returned will be equal to the request's {@link DataSpec#length}.
|
* unresolved. For all other requests, the value returned will be equal to the request's
|
||||||
|
* {@link DataSpec#length}.
|
||||||
*/
|
*/
|
||||||
public long open(DataSpec dataSpec) throws IOException;
|
public long open(DataSpec dataSpec) throws IOException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
@ -39,6 +40,8 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int CHUNKED_ALLOCATION_INCREMENT = 256 * 1024;
|
||||||
|
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final DataSpec dataSpec;
|
private final DataSpec dataSpec;
|
||||||
private final Allocator allocator;
|
private final Allocator allocator;
|
||||||
|
|
@ -57,7 +60,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
/**
|
/**
|
||||||
* @param dataSource The source from which the data should be loaded.
|
* @param dataSource The source from which the data should be loaded.
|
||||||
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
|
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
|
||||||
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then
|
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
|
||||||
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
||||||
* {@link Integer#MAX_VALUE}.
|
* {@link Integer#MAX_VALUE}.
|
||||||
* @param allocator Used to obtain an {@link Allocation} for holding the data.
|
* @param allocator Used to obtain an {@link Allocation} for holding the data.
|
||||||
|
|
@ -67,7 +70,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.dataSpec = dataSpec;
|
this.dataSpec = dataSpec;
|
||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
resolvedLength = DataSpec.LENGTH_UNBOUNDED;
|
resolvedLength = C.LENGTH_UNBOUNDED;
|
||||||
readHead = new ReadHead();
|
readHead = new ReadHead();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,13 +100,14 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the length of the streamin bytes.
|
* Returns the length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has
|
||||||
|
* yet to be determined.
|
||||||
*
|
*
|
||||||
* @return The length of the stream in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length
|
* @return The length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has
|
||||||
* has yet to be determined.
|
* yet to be determined.
|
||||||
*/
|
*/
|
||||||
public long getLength() {
|
public long getLength() {
|
||||||
return resolvedLength != DataSpec.LENGTH_UNBOUNDED ? resolvedLength : dataSpec.length;
|
return resolvedLength != C.LENGTH_UNBOUNDED ? resolvedLength : dataSpec.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,7 +116,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
* @return True if the stream has finished loading. False otherwise.
|
* @return True if the stream has finished loading. False otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean isLoadFinished() {
|
public boolean isLoadFinished() {
|
||||||
return resolvedLength != DataSpec.LENGTH_UNBOUNDED && loadPosition == resolvedLength;
|
return resolvedLength != C.LENGTH_UNBOUNDED && loadPosition == resolvedLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -123,7 +127,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
* Note: The read methods provide a more efficient way of consuming the loaded data. Use this
|
* Note: The read methods provide a more efficient way of consuming the loaded data. Use this
|
||||||
* method only when a freshly allocated byte[] containing all of the loaded data is required.
|
* method only when a freshly allocated byte[] containing all of the loaded data is required.
|
||||||
*
|
*
|
||||||
* @return The loaded data or null.
|
* @return The loaded data, or null.
|
||||||
*/
|
*/
|
||||||
public final byte[] getLoadedData() {
|
public final byte[] getLoadedData() {
|
||||||
if (loadPosition == 0) {
|
if (loadPosition == 0) {
|
||||||
|
|
@ -144,7 +148,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEndOfStream() {
|
public boolean isEndOfStream() {
|
||||||
return resolvedLength != DataSpec.LENGTH_UNBOUNDED && readHead.position == resolvedLength;
|
return resolvedLength != C.LENGTH_UNBOUNDED && readHead.position == resolvedLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -191,6 +195,11 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
int bytesRead = 0;
|
int bytesRead = 0;
|
||||||
byte[][] buffers = allocation.getBuffers();
|
byte[][] buffers = allocation.getBuffers();
|
||||||
while (bytesRead < bytesToRead) {
|
while (bytesRead < bytesToRead) {
|
||||||
|
if (readHead.fragmentRemaining == 0) {
|
||||||
|
readHead.fragmentIndex++;
|
||||||
|
readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
|
||||||
|
readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
|
||||||
|
}
|
||||||
int bufferReadLength = Math.min(readHead.fragmentRemaining, bytesToRead - bytesRead);
|
int bufferReadLength = Math.min(readHead.fragmentRemaining, bytesToRead - bytesRead);
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength);
|
target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength);
|
||||||
|
|
@ -203,11 +212,6 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
bytesRead += bufferReadLength;
|
bytesRead += bufferReadLength;
|
||||||
readHead.fragmentOffset += bufferReadLength;
|
readHead.fragmentOffset += bufferReadLength;
|
||||||
readHead.fragmentRemaining -= bufferReadLength;
|
readHead.fragmentRemaining -= bufferReadLength;
|
||||||
if (readHead.fragmentRemaining == 0 && readHead.position < resolvedLength) {
|
|
||||||
readHead.fragmentIndex++;
|
|
||||||
readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
|
|
||||||
readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
|
|
@ -231,23 +235,32 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
// The load was canceled, or is already complete.
|
// The load was canceled, or is already complete.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DataSpec loadDataSpec;
|
DataSpec loadDataSpec;
|
||||||
if (resolvedLength == DataSpec.LENGTH_UNBOUNDED) {
|
if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) {
|
||||||
loadDataSpec = dataSpec;
|
loadDataSpec = dataSpec;
|
||||||
resolvedLength = dataSource.open(loadDataSpec);
|
long resolvedLength = dataSource.open(loadDataSpec);
|
||||||
if (resolvedLength > Integer.MAX_VALUE) {
|
if (resolvedLength > Integer.MAX_VALUE) {
|
||||||
throw new DataSourceStreamLoadException(
|
throw new DataSourceStreamLoadException(
|
||||||
new UnexpectedLengthException(dataSpec.length, resolvedLength));
|
new UnexpectedLengthException(dataSpec.length, resolvedLength));
|
||||||
}
|
}
|
||||||
|
this.resolvedLength = resolvedLength;
|
||||||
} else {
|
} else {
|
||||||
|
long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED
|
||||||
|
? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED;
|
||||||
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
|
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
|
||||||
resolvedLength - loadPosition, dataSpec.key);
|
remainingLength, dataSpec.key);
|
||||||
dataSource.open(loadDataSpec);
|
dataSource.open(loadDataSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allocation == null) {
|
if (allocation == null) {
|
||||||
allocation = allocator.allocate((int) resolvedLength);
|
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
|
||||||
|
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
|
||||||
|
allocation = allocator.allocate(initialAllocationSize);
|
||||||
}
|
}
|
||||||
|
int allocationCapacity = allocation.capacity();
|
||||||
|
|
||||||
if (loadPosition == 0) {
|
if (loadPosition == 0) {
|
||||||
writeFragmentIndex = 0;
|
writeFragmentIndex = 0;
|
||||||
writeFragmentOffset = allocation.getFragmentOffset(0);
|
writeFragmentOffset = allocation.getFragmentOffset(0);
|
||||||
|
|
@ -256,22 +269,28 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
|
|
||||||
int read = Integer.MAX_VALUE;
|
int read = Integer.MAX_VALUE;
|
||||||
byte[][] buffers = allocation.getBuffers();
|
byte[][] buffers = allocation.getBuffers();
|
||||||
while (!loadCanceled && loadPosition < resolvedLength && read > 0) {
|
while (!loadCanceled && read > 0 && maybeMoreToLoad()) {
|
||||||
if (Thread.interrupted()) {
|
if (Thread.interrupted()) {
|
||||||
throw new InterruptedException();
|
throw new InterruptedException();
|
||||||
}
|
}
|
||||||
int writeLength = (int) Math.min(writeFragmentRemainingLength,
|
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset,
|
||||||
resolvedLength - loadPosition);
|
writeFragmentRemainingLength);
|
||||||
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, writeLength);
|
|
||||||
if (read > 0) {
|
if (read > 0) {
|
||||||
loadPosition += read;
|
loadPosition += read;
|
||||||
writeFragmentOffset += read;
|
writeFragmentOffset += read;
|
||||||
writeFragmentRemainingLength -= read;
|
writeFragmentRemainingLength -= read;
|
||||||
if (writeFragmentRemainingLength == 0 && loadPosition < resolvedLength) {
|
if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) {
|
||||||
writeFragmentIndex++;
|
writeFragmentIndex++;
|
||||||
|
if (loadPosition == allocationCapacity) {
|
||||||
|
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
|
||||||
|
allocationCapacity = allocation.capacity();
|
||||||
|
buffers = allocation.getBuffers();
|
||||||
|
}
|
||||||
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
|
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
|
||||||
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
|
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
|
||||||
}
|
}
|
||||||
|
} else if (resolvedLength == C.LENGTH_UNBOUNDED) {
|
||||||
|
resolvedLength = loadPosition;
|
||||||
} else if (resolvedLength != loadPosition) {
|
} else if (resolvedLength != loadPosition) {
|
||||||
throw new DataSourceStreamLoadException(
|
throw new DataSourceStreamLoadException(
|
||||||
new UnexpectedLengthException(resolvedLength, loadPosition));
|
new UnexpectedLengthException(resolvedLength, loadPosition));
|
||||||
|
|
@ -282,6 +301,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean maybeMoreToLoad() {
|
||||||
|
return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength;
|
||||||
|
}
|
||||||
|
|
||||||
private static class ReadHead {
|
private static class ReadHead {
|
||||||
|
|
||||||
private int position;
|
private int position;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
@ -24,13 +25,6 @@ import android.net.Uri;
|
||||||
*/
|
*/
|
||||||
public final class DataSpec {
|
public final class DataSpec {
|
||||||
|
|
||||||
/**
|
|
||||||
* A permitted value of {@link #length}. A {@link DataSpec} defined with this length represents
|
|
||||||
* the region of media data that starts at its {@link #position} and extends to the end of the
|
|
||||||
* data whose location is defined by its {@link #uri}.
|
|
||||||
*/
|
|
||||||
public static final int LENGTH_UNBOUNDED = -1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies the source from which data should be read.
|
* Identifies the source from which data should be read.
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,7 +44,7 @@ public final class DataSpec {
|
||||||
*/
|
*/
|
||||||
public final long position;
|
public final long position;
|
||||||
/**
|
/**
|
||||||
* The length of the data. Greater than zero, or equal to {@link #LENGTH_UNBOUNDED}.
|
* The length of the data. Greater than zero, or equal to {@link C#LENGTH_UNBOUNDED}.
|
||||||
*/
|
*/
|
||||||
public final long length;
|
public final long length;
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,7 +92,7 @@ public final class DataSpec {
|
||||||
boolean uriIsFullStream) {
|
boolean uriIsFullStream) {
|
||||||
Assertions.checkArgument(absoluteStreamPosition >= 0);
|
Assertions.checkArgument(absoluteStreamPosition >= 0);
|
||||||
Assertions.checkArgument(position >= 0);
|
Assertions.checkArgument(position >= 0);
|
||||||
Assertions.checkArgument(length > 0 || length == LENGTH_UNBOUNDED);
|
Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED);
|
||||||
Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
|
Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.uriIsFullStream = uriIsFullStream;
|
this.uriIsFullStream = uriIsFullStream;
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,11 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
||||||
*
|
*
|
||||||
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
|
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
|
||||||
* @param bytes The number of bytes transferred.
|
* @param bytes The number of bytes transferred.
|
||||||
* @param bandwidthEstimate The estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no
|
* @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
|
||||||
* estimate is available. Note that this estimate is typically derived from more information
|
* is available. Note that this estimate is typically derived from more information than
|
||||||
* than {@code bytes} and {@code elapsedMs}.
|
* {@code bytes} and {@code elapsedMs}.
|
||||||
*/
|
*/
|
||||||
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
|
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,9 +53,9 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final SlidingPercentile slidingPercentile;
|
private final SlidingPercentile slidingPercentile;
|
||||||
|
|
||||||
private long accumulator;
|
private long bytesAccumulator;
|
||||||
private long startTimeMs;
|
private long startTimeMs;
|
||||||
private long bandwidthEstimate;
|
private long bitrateEstimate;
|
||||||
private int streamCount;
|
private int streamCount;
|
||||||
|
|
||||||
public DefaultBandwidthMeter() {
|
public DefaultBandwidthMeter() {
|
||||||
|
|
@ -80,17 +80,12 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.slidingPercentile = new SlidingPercentile(maxWeight);
|
this.slidingPercentile = new SlidingPercentile(maxWeight);
|
||||||
bandwidthEstimate = NO_ESTIMATE;
|
bitrateEstimate = NO_ESTIMATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the estimated bandwidth.
|
|
||||||
*
|
|
||||||
* @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized long getEstimate() {
|
public synchronized long getBitrateEstimate() {
|
||||||
return bandwidthEstimate;
|
return bitrateEstimate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -103,7 +98,7 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void onBytesTransferred(int bytes) {
|
public synchronized void onBytesTransferred(int bytes) {
|
||||||
accumulator += bytes;
|
bytesAccumulator += bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -112,32 +107,26 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
||||||
long nowMs = clock.elapsedRealtime();
|
long nowMs = clock.elapsedRealtime();
|
||||||
int elapsedMs = (int) (nowMs - startTimeMs);
|
int elapsedMs = (int) (nowMs - startTimeMs);
|
||||||
if (elapsedMs > 0) {
|
if (elapsedMs > 0) {
|
||||||
float bytesPerSecond = accumulator * 1000 / elapsedMs;
|
float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs;
|
||||||
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
|
slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond);
|
||||||
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
|
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
|
||||||
bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
|
bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
|
||||||
: (long) bandwidthEstimateFloat;
|
: (long) bandwidthEstimateFloat;
|
||||||
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
|
notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate);
|
||||||
}
|
}
|
||||||
streamCount--;
|
streamCount--;
|
||||||
if (streamCount > 0) {
|
if (streamCount > 0) {
|
||||||
startTimeMs = nowMs;
|
startTimeMs = nowMs;
|
||||||
}
|
}
|
||||||
accumulator = 0;
|
bytesAccumulator = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use media time (bytes / mediaRate) as weight.
|
private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
|
||||||
private int computeWeight(long mediaBytes) {
|
|
||||||
return (int) Math.sqrt(mediaBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyBandwidthSample(final int elapsedMs, final long bytes,
|
|
||||||
final long bandwidthEstimate) {
|
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
eventListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate);
|
eventListener.onBandwidthSample(elapsedMs, bytes, bitrate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
|
@ -42,8 +44,7 @@ public final class FileDataSource implements DataSource {
|
||||||
try {
|
try {
|
||||||
file = new RandomAccessFile(dataSpec.uri.getPath(), "r");
|
file = new RandomAccessFile(dataSpec.uri.getPath(), "r");
|
||||||
file.seek(dataSpec.position);
|
file.seek(dataSpec.position);
|
||||||
bytesRemaining = dataSpec.length == DataSpec.LENGTH_UNBOUNDED
|
bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position
|
||||||
? file.length() - dataSpec.position
|
|
||||||
: dataSpec.length;
|
: dataSpec.length;
|
||||||
return bytesRemaining;
|
return bytesRemaining;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.Predicate;
|
import com.google.android.exoplayer.util.Predicate;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
@ -258,16 +259,9 @@ public class HttpDataSource implements DataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
long contentLength = getContentLength(connection);
|
long contentLength = getContentLength(connection);
|
||||||
dataLength = dataSpec.length == DataSpec.LENGTH_UNBOUNDED ? contentLength : dataSpec.length;
|
dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length;
|
||||||
if (dataLength == DataSpec.LENGTH_UNBOUNDED) {
|
|
||||||
// The DataSpec specified unbounded length and we failed to resolve a length from the
|
|
||||||
// response headers.
|
|
||||||
throw new HttpDataSourceException(
|
|
||||||
new UnexpectedLengthException(DataSpec.LENGTH_UNBOUNDED, DataSpec.LENGTH_UNBOUNDED),
|
|
||||||
dataSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED && contentLength != DataSpec.LENGTH_UNBOUNDED
|
if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED
|
||||||
&& contentLength != dataSpec.length) {
|
&& contentLength != dataSpec.length) {
|
||||||
// The DataSpec specified a length and we resolved a length from the response headers, but
|
// The DataSpec specified a length and we resolved a length from the response headers, but
|
||||||
// the two lengths do not match.
|
// the two lengths do not match.
|
||||||
|
|
@ -305,9 +299,9 @@ public class HttpDataSource implements DataSource {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onBytesTransferred(read);
|
listener.onBytesTransferred(read);
|
||||||
}
|
}
|
||||||
} else if (dataLength != bytesRead) {
|
} else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) {
|
||||||
// Check for cases where the server closed the connection having not sent the correct amount
|
// Check for cases where the server closed the connection having not sent the correct amount
|
||||||
// of data.
|
// of data. We can only do this if we know the length of the data we were expecting.
|
||||||
throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
|
throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
|
||||||
dataSpec);
|
dataSpec);
|
||||||
}
|
}
|
||||||
|
|
@ -364,14 +358,15 @@ public class HttpDataSource implements DataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of bytes that are still to be read for the current {@link DataSpec}. This
|
* Returns the number of bytes that are still to be read for the current {@link DataSpec}.
|
||||||
* value is equivalent to {@code dataSpec.length - bytesRead()}, where dataSpec is the
|
* <p>
|
||||||
* {@link DataSpec} that was passed to the most recent call of {@link #open(DataSpec)}.
|
* If the total length of the data being read is known, then this length minus {@code bytesRead()}
|
||||||
|
* is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned.
|
||||||
*
|
*
|
||||||
* @return The number of bytes remaining.
|
* @return The remaining length, or {@link C#LENGTH_UNBOUNDED}.
|
||||||
*/
|
*/
|
||||||
protected final long bytesRemaining() {
|
protected final long bytesRemaining() {
|
||||||
return dataLength - bytesRead;
|
return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
|
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
|
||||||
|
|
@ -394,14 +389,14 @@ public class HttpDataSource implements DataSource {
|
||||||
|
|
||||||
private String buildRangeHeader(DataSpec dataSpec) {
|
private String buildRangeHeader(DataSpec dataSpec) {
|
||||||
String rangeRequest = "bytes=" + dataSpec.position + "-";
|
String rangeRequest = "bytes=" + dataSpec.position + "-";
|
||||||
if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED) {
|
if (dataSpec.length != C.LENGTH_UNBOUNDED) {
|
||||||
rangeRequest += (dataSpec.position + dataSpec.length - 1);
|
rangeRequest += (dataSpec.position + dataSpec.length - 1);
|
||||||
}
|
}
|
||||||
return rangeRequest;
|
return rangeRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getContentLength(HttpURLConnection connection) {
|
private long getContentLength(HttpURLConnection connection) {
|
||||||
long contentLength = DataSpec.LENGTH_UNBOUNDED;
|
long contentLength = C.LENGTH_UNBOUNDED;
|
||||||
String contentLengthHeader = connection.getHeaderField("Content-Length");
|
String contentLengthHeader = connection.getHeaderField("Content-Length");
|
||||||
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -435,10 +430,6 @@ public class HttpDataSource implements DataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contentLength == DataSpec.LENGTH_UNBOUNDED) {
|
|
||||||
Log.w(TAG, "Unable to parse content length [" + contentLengthHeader + "] [" +
|
|
||||||
contentRangeHeader + "]");
|
|
||||||
}
|
|
||||||
return contentLength;
|
return contentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream;
|
package com.google.android.exoplayer.upstream;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -39,7 +40,7 @@ public final class TeeDataSource implements DataSource {
|
||||||
@Override
|
@Override
|
||||||
public long open(DataSpec dataSpec) throws IOException {
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
long dataLength = upstream.open(dataSpec);
|
long dataLength = upstream.open(dataSpec);
|
||||||
if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) {
|
if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) {
|
||||||
// Reconstruct dataSpec in order to provide the resolved length to the sink.
|
// Reconstruct dataSpec in order to provide the resolved length to the sink.
|
||||||
dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength,
|
dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength,
|
||||||
dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream);
|
dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream.cache;
|
package com.google.android.exoplayer.upstream.cache;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.upstream.DataSink;
|
import com.google.android.exoplayer.upstream.DataSink;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
@ -63,6 +64,9 @@ public class CacheDataSink implements DataSink {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DataSink open(DataSpec dataSpec) throws CacheDataSinkException {
|
public DataSink open(DataSpec dataSpec) throws CacheDataSinkException {
|
||||||
|
// TODO: Support caching for unbounded requests. See TODO in {@link CacheDataSource} for
|
||||||
|
// more details.
|
||||||
|
Assertions.checkState(dataSpec.length != C.LENGTH_UNBOUNDED);
|
||||||
try {
|
try {
|
||||||
this.dataSpec = dataSpec;
|
this.dataSpec = dataSpec;
|
||||||
dataSpecBytesWritten = 0;
|
dataSpecBytesWritten = 0;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.upstream.cache;
|
package com.google.android.exoplayer.upstream.cache;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.upstream.DataSink;
|
import com.google.android.exoplayer.upstream.DataSink;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
|
|
@ -34,10 +35,26 @@ import java.io.IOException;
|
||||||
*/
|
*/
|
||||||
public final class CacheDataSource implements DataSource {
|
public final class CacheDataSource implements DataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface definition for a callback to be notified of {@link CacheDataSource} events.
|
||||||
|
*/
|
||||||
|
public interface EventListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when bytes have been read from {@link #cache} since the last invocation.
|
||||||
|
*
|
||||||
|
* @param cacheSizeBytes Current cache size in bytes.
|
||||||
|
* @param cachedBytesRead Total bytes read from {@link #cache} since last report.
|
||||||
|
*/
|
||||||
|
void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private final Cache cache;
|
private final Cache cache;
|
||||||
private final DataSource cacheReadDataSource;
|
private final DataSource cacheReadDataSource;
|
||||||
private final DataSource cacheWriteDataSource;
|
private final DataSource cacheWriteDataSource;
|
||||||
private final DataSource upstreamDataSource;
|
private final DataSource upstreamDataSource;
|
||||||
|
private final EventListener eventListener;
|
||||||
|
|
||||||
private final boolean blockOnCache;
|
private final boolean blockOnCache;
|
||||||
private final boolean ignoreCacheOnError;
|
private final boolean ignoreCacheOnError;
|
||||||
|
|
@ -49,6 +66,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
private long bytesRemaining;
|
private long bytesRemaining;
|
||||||
private CacheSpan lockedSpan;
|
private CacheSpan lockedSpan;
|
||||||
private boolean ignoreCache;
|
private boolean ignoreCache;
|
||||||
|
private long totalCachedBytesRead;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
|
* Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
|
||||||
|
|
@ -67,7 +85,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache,
|
public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache,
|
||||||
boolean ignoreCacheOnError, long maxCacheFileSize) {
|
boolean ignoreCacheOnError, long maxCacheFileSize) {
|
||||||
this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize),
|
this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize),
|
||||||
blockOnCache, ignoreCacheOnError);
|
blockOnCache, ignoreCacheOnError, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,9 +102,11 @@ public final class CacheDataSource implements DataSource {
|
||||||
* @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If
|
* @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If
|
||||||
* true, then cache related exceptions may be thrown for one cycle of open, read and close
|
* true, then cache related exceptions may be thrown for one cycle of open, read and close
|
||||||
* calls. Subsequent cycles of these calls will then bypass the cache.
|
* calls. Subsequent cycles of these calls will then bypass the cache.
|
||||||
|
* @param eventListener An optional {@link EventListener} to receive events.
|
||||||
*/
|
*/
|
||||||
public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
|
public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
|
||||||
DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError) {
|
DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError,
|
||||||
|
EventListener eventListener) {
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.cacheReadDataSource = cacheReadDataSource;
|
this.cacheReadDataSource = cacheReadDataSource;
|
||||||
this.blockOnCache = blockOnCache;
|
this.blockOnCache = blockOnCache;
|
||||||
|
|
@ -97,6 +117,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
} else {
|
} else {
|
||||||
this.cacheWriteDataSource = null;
|
this.cacheWriteDataSource = null;
|
||||||
}
|
}
|
||||||
|
this.eventListener = eventListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -104,7 +125,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
Assertions.checkState(dataSpec.uriIsFullStream);
|
Assertions.checkState(dataSpec.uriIsFullStream);
|
||||||
// TODO: Support caching for unbounded requests. This requires storing the source length
|
// TODO: Support caching for unbounded requests. This requires storing the source length
|
||||||
// into the cache (the simplest approach is to incorporate it into each cache file's name).
|
// into the cache (the simplest approach is to incorporate it into each cache file's name).
|
||||||
Assertions.checkState(dataSpec.length != DataSpec.LENGTH_UNBOUNDED);
|
Assertions.checkState(dataSpec.length != C.LENGTH_UNBOUNDED);
|
||||||
try {
|
try {
|
||||||
uri = dataSpec.uri;
|
uri = dataSpec.uri;
|
||||||
key = dataSpec.key;
|
key = dataSpec.key;
|
||||||
|
|
@ -121,10 +142,13 @@ public final class CacheDataSource implements DataSource {
|
||||||
@Override
|
@Override
|
||||||
public int read(byte[] buffer, int offset, int max) throws IOException {
|
public int read(byte[] buffer, int offset, int max) throws IOException {
|
||||||
try {
|
try {
|
||||||
int num = currentDataSource.read(buffer, offset, max);
|
int bytesRead = currentDataSource.read(buffer, offset, max);
|
||||||
if (num >= 0) {
|
if (bytesRead >= 0) {
|
||||||
readPosition += num;
|
if (currentDataSource == cacheReadDataSource) {
|
||||||
bytesRemaining -= num;
|
totalCachedBytesRead += bytesRead;
|
||||||
|
}
|
||||||
|
readPosition += bytesRead;
|
||||||
|
bytesRemaining -= bytesRead;
|
||||||
} else {
|
} else {
|
||||||
closeCurrentSource();
|
closeCurrentSource();
|
||||||
if (bytesRemaining > 0) {
|
if (bytesRemaining > 0) {
|
||||||
|
|
@ -132,7 +156,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
return read(buffer, offset, max);
|
return read(buffer, offset, max);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return num;
|
return bytesRead;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
handleBeforeThrow(e);
|
handleBeforeThrow(e);
|
||||||
throw e;
|
throw e;
|
||||||
|
|
@ -141,6 +165,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
|
notifyBytesRead();
|
||||||
try {
|
try {
|
||||||
closeCurrentSource();
|
closeCurrentSource();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -215,4 +240,11 @@ public final class CacheDataSource implements DataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyBytesRead() {
|
||||||
|
if (eventListener != null && totalCachedBytesRead > 0) {
|
||||||
|
eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead);
|
||||||
|
totalCachedBytesRead = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue