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