From 50f69f22dfe35dc65119b03454410f24998755c1 Mon Sep 17 00:00:00 2001 From: Kentaro Takiguchi Date: Tue, 15 Jul 2014 17:35:26 +0900 Subject: [PATCH 01/22] Add gitignore --- .gitignore | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..16f7e8aec9 --- /dev/null +++ b/.gitignore @@ -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 From 5a888f3a6de3694e7da4b56b2939d2ae50bedc63 Mon Sep 17 00:00:00 2001 From: Kentaro Takiguchi Date: Tue, 15 Jul 2014 17:38:50 +0900 Subject: [PATCH 02/22] Update tools version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2d29f854be..a444cfb512 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.10.+' + classpath 'com.android.tools.build:gradle:0.12.+' } } From fa83b2fa5e9b7e348b763c1f6b1822e8b6cf9340 Mon Sep 17 00:00:00 2001 From: Kentaro Takiguchi Date: Tue, 15 Jul 2014 17:43:34 +0900 Subject: [PATCH 03/22] Add jarRelease task --- library/build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/library/build.gradle b/library/build.gradle index 5b751a0820..68a489b08a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -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); +} From e33f5a514c79f3b3126a7f12aa668fe3aea61c3b Mon Sep 17 00:00:00 2001 From: rejasupotaro Date: Tue, 15 Jul 2014 17:49:46 +0900 Subject: [PATCH 04/22] Update README.md --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7efc281f85..6faf3a264b 100644 --- a/README.md +++ b/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. From b3277c666b14651023b42eecb3ce5e436d62af33 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 28 Jul 2014 14:29:21 +0100 Subject: [PATCH 05/22] Add language to Format (+other misc fix). --- .../exoplayer/MediaCodecTrackRenderer.java | 16 +++++++++---- .../android/exoplayer/chunk/Format.java | 24 +++++++++++++++++++ .../MediaPresentationDescriptionParser.java | 9 +++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 99a8bee91e..ca9f0f6a80 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -280,14 +280,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() { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java index 875956c0ee..3482d160fc 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java @@ -71,6 +71,14 @@ public class Format { */ public final int bitrate; + /** + * The language of the format. Can be null if unknown. + *

+ * 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; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 3bf9666006..2bd53d998b 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -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); } From ad26085e5c7a911101fd2415db3b2c45a129a53f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 Aug 2014 15:51:21 +0100 Subject: [PATCH 06/22] Finish painful bytes/sec -> bits/sec conversion. --- .../exoplayer/demo/full/EventLogger.java | 4 +- .../demo/full/player/DemoPlayer.java | 6 +-- .../exoplayer/chunk/FormatEvaluator.java | 26 +++++------ .../exoplayer/upstream/BandwidthMeter.java | 6 +-- .../upstream/DefaultBandwidthMeter.java | 45 +++++++------------ 5 files changed, 38 insertions(+), 49 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java index f8306d10d1..2098997297 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java @@ -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 diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index acf69656ed..63a2557541 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -121,7 +121,7 @@ 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); @@ -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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java index 1a87b9a142..d64ca8a262 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java @@ -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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java index 0007597e68..5bbffb6c1f 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java @@ -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(); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java index 4b2ed2806c..ce2197e2cf 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java @@ -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); } }); } From 32464e6de4f6a79bffc4e634f2dc59dfae3d4a9d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 Aug 2014 15:51:48 +0100 Subject: [PATCH 07/22] Fix integer variant of library version. --- .../java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 4b50a56517..4661568d1e 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -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 = 1000011; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} From 2a82ff353b8b8a613a874a8d75e39b485a1a68a0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 Aug 2014 15:53:08 +0100 Subject: [PATCH 08/22] Make DefaultWebmExtractor handle cues and format independently. * Remove concept of being prepared by simply reporting if format and/or cues are known. * Allow replacement of format and/or cues later in the stream. * Initialization and index segments can be parsed independently of one another but must be in order due to internal WebM dependencies. * Let seekTo() work even when cues are unknown. --- .../parser/webm/DefaultWebmExtractor.java | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java index 351eff32d9..8203fffd13 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java @@ -83,7 +83,6 @@ public final class DefaultWebmExtractor implements WebmExtractor { private SampleHolder tempSampleHolder; private boolean sampleRead; - private boolean prepared = false; private long segmentStartOffsetBytes = UNKNOWN; private long segmentEndOffsetBytes = UNKNOWN; private long timecodeScale = 1000000L; @@ -105,13 +104,11 @@ 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; + return format != null && cues != null; } @Override @@ -125,8 +122,9 @@ public final class DefaultWebmExtractor implements WebmExtractor { @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 +132,19 @@ public final class DefaultWebmExtractor implements WebmExtractor { return false; } } + clusterTimecodeUs = UNKNOWN; + simpleBlockTimecodeUs = UNKNOWN; reader.reset(); return true; } @Override public SegmentIndex getCues() { - checkPrepared(); return cues; } @Override public MediaFormat getFormat() { - checkPrepared(); return format; } @@ -196,6 +194,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 +204,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) { @@ -330,30 +335,40 @@ public final class DefaultWebmExtractor implements WebmExtractor { 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. + * + *

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); + } 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. + * + *

{@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 +387,6 @@ public final class DefaultWebmExtractor implements WebmExtractor { cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs); cueTimesUs = null; cueClusterPositions = null; - - prepared = true; } /** From b1992c384895637ed232a93b193ac16a72f48e44 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 Aug 2014 15:53:49 +0100 Subject: [PATCH 09/22] Don't call doSomeWork if we're preparing --- .../com/google/android/exoplayer/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index c6858eef3c..f1a0963969 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -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); } From 41ff1e4071d3e06a3bb7e0e542bae7d5bfc8a60b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 Aug 2014 15:54:32 +0100 Subject: [PATCH 10/22] Add CacheDataSource.Listener. --- .../upstream/cache/CacheDataSource.java | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java index 1a0ebf3e19..1f82252540 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java @@ -34,10 +34,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 +65,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 +84,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 +101,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 +116,7 @@ public final class CacheDataSource implements DataSource { } else { this.cacheWriteDataSource = null; } + this.eventListener = eventListener; } @Override @@ -121,10 +141,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 +155,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 +164,7 @@ public final class CacheDataSource implements DataSource { @Override public void close() throws IOException { + notifyBytesRead(); try { closeCurrentSource(); } catch (IOException e) { @@ -215,4 +239,11 @@ public final class CacheDataSource implements DataSource { } } + private void notifyBytesRead() { + if (eventListener != null && totalCachedBytesRead > 0) { + eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead); + totalCachedBytesRead = 0; + } + } + } From 9a124120ff3f2759fb08c0ff05b45c363cf6d065 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 Aug 2014 15:56:26 +0100 Subject: [PATCH 11/22] Changes around renderer readiness and extraction. - Make MediaCodecTrackRenderer.isReady more permissive. This largely fixes #21 - Bring WebmExtractor closer to FragmentedMp4Extractor. The two will probably be placed under a common interface fairly soon, which will allow significant code deduplication. --- .../exoplayer/FrameworkSampleSource.java | 40 ++--- .../MediaCodecAudioTrackRenderer.java | 2 +- .../exoplayer/MediaCodecTrackRenderer.java | 10 +- .../MediaCodecVideoTrackRenderer.java | 2 +- .../android/exoplayer/SampleSource.java | 5 +- .../exoplayer/chunk/ChunkSampleSource.java | 12 +- .../android/exoplayer/chunk/MediaChunk.java | 8 + .../exoplayer/chunk/Mp4MediaChunk.java | 9 +- .../chunk/SingleSampleMediaChunk.java | 7 +- .../exoplayer/chunk/WebmMediaChunk.java | 11 +- .../exoplayer/dash/DashMp4ChunkSource.java | 12 +- .../exoplayer/dash/DashWebmChunkSource.java | 76 +++++--- .../parser/mp4/FragmentedMp4Extractor.java | 168 +++++++----------- .../parser/webm/DefaultEbmlReader.java | 60 +++---- .../parser/webm/DefaultWebmExtractor.java | 92 ++++++---- .../parser/webm/EbmlEventHandler.java | 25 ++- .../exoplayer/parser/webm/EbmlReader.java | 4 +- .../exoplayer/parser/webm/WebmExtractor.java | 36 ++-- .../exoplayer/text/TextTrackRenderer.java | 7 +- 19 files changed, 317 insertions(+), 269 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 653dc2d628..bc1ee49a95 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -67,7 +67,7 @@ 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); @@ -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); @@ -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; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index 2ffe4a53f6..c02e8a9135 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -417,7 +417,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean isReady() { - return getPendingFrameCount() > 0; + return super.isReady() || getPendingFrameCount() > 0; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index ca9f0f6a80..9073fa93b5 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -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; @@ -196,6 +197,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; @@ -346,6 +348,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; @@ -364,7 +367,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(); @@ -645,10 +648,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() { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 98ec611d2a..b941767955 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 9c5d6aa303..56b20bf8a7 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -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. diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 61ad0c93ee..40e1544bcc 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -246,11 +246,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 diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java index 51ebac65c2..e03a529d8c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java @@ -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. *

diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java index 8aaee879e4..01033e73f6 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java index 6fa2f08962..dfe0d71584 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java @@ -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(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java index f7ca26244a..4769da9772 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java @@ -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 diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java index e76152859f..0013df15a4 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java @@ -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)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java index 133b4879ac..2f01a38120 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java @@ -56,21 +56,26 @@ public class DashWebmChunkSource implements ChunkSource { private final Format[] formats; private final HashMap representations; - private final HashMap extractors; + private final HashMap extractors; private final HashMap 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(); + this.extractors = new HashMap(); this.segmentIndexes = new HashMap(); this.representations = new HashMap(); - 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 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)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 16e1788943..7a990ee2a7 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -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}; @@ -98,8 +94,7 @@ public final class FragmentedMp4Extractor { 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_SAMPLE = 3; // Atom data offsets private static final int ATOM_HEADER_SIZE = 8; @@ -169,7 +164,6 @@ public final class FragmentedMp4Extractor { private ParsableByteArray atomData; private ParsableByteArray cencAuxiliaryData; private int cencAuxiliaryBytesRead; - private int sampleBytesRead; private int pendingSeekTimeMs; private int sampleIndex; @@ -207,7 +201,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 +239,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 +254,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. - *

- * 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 @@ -353,9 +333,6 @@ public final class FragmentedMp4Extractor { case STATE_READING_CENC_AUXILIARY_DATA: cencAuxiliaryBytesRead = 0; break; - case STATE_READING_SAMPLE_START: - sampleBytesRead = 0; - break; } parserState = state; } @@ -383,7 +360,7 @@ public final class FragmentedMp4Extractor { enterState(STATE_READING_CENC_AUXILIARY_DATA); } else { cencAuxiliaryData = null; - enterState(STATE_READING_SAMPLE_START); + enterState(STATE_READING_SAMPLE); } return 0; } @@ -442,7 +419,7 @@ public final class FragmentedMp4Extractor { containerAtoms.peek().add(leaf); } else if (leaf.type == Atom.TYPE_sidx) { segmentIndex = parseSidx(leaf.getData()); - return RESULT_READ_SIDX; + return RESULT_READ_INDEX; } return 0; } @@ -450,7 +427,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()) { @@ -1078,7 +1055,7 @@ public final class FragmentedMp4Extractor { if (cencAuxiliaryBytesRead < length) { return RESULT_NEED_MORE_DATA; } - enterState(STATE_READING_SAMPLE_START); + enterState(STATE_READING_SAMPLE); return 0; } @@ -1105,89 +1082,68 @@ 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) { + 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); } } - 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; + } + ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData + : fragmentRun.smoothStreamingSampleEncryptionData; + if (sampleEncryptionData != null) { + readSampleEncryptionData(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 +1159,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") diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java index f66b83293f..55eca63de6 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java index 8203fffd13..00f24bbad7 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java @@ -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,11 +78,14 @@ 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 long segmentStartOffsetBytes = UNKNOWN; private long segmentEndOffsetBytes = UNKNOWN; @@ -107,17 +111,19 @@ public final class DefaultWebmExtractor implements WebmExtractor { } @Override - public boolean isPrepared() { - return format != null && cues != null; - } - - @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 @@ -139,7 +145,7 @@ public final class DefaultWebmExtractor implements WebmExtractor { } @Override - public SegmentIndex getCues() { + public SegmentIndex getIndex() { return cues; } @@ -288,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); @@ -309,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: @@ -321,14 +333,22 @@ 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) { @@ -347,6 +367,7 @@ public final class DefaultWebmExtractor implements WebmExtractor { && (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"); } @@ -387,6 +408,7 @@ public final class DefaultWebmExtractor implements WebmExtractor { cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs); cueTimesUs = null; cueClusterPositions = null; + readResults |= RESULT_READ_INDEX; } /** @@ -401,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 diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java index 42e26d4531..dcedf9a898 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java @@ -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}. * *

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}. + * + *

It's guaranteed that the full element contents will be immediately available from + * {@code inputStream}. * *

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, diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java index a9bf11f699..dd1c43fce3 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java @@ -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 diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java index 4ecefe7906..e824887476 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java @@ -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}. * - *

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. diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 3e671d2302..6f1e6f96ca 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -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 From 25a532656c25d5ce82e0f1b5cddc3e2e9b8f7e73 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 Aug 2014 17:42:08 +0100 Subject: [PATCH 12/22] Optimize some CodecCounter inefficiency. 1. Use ints rather than longs. 2. Remove some counters that dont seem hugely useful. 3. Replace use of volatile with explicit method calls that cause a memory barrier. This is a lot more efficient than using volatile because it can be invoked only once per doSomeWork. --- .../android/exoplayer/CodecCounters.java | 49 +++++++------------ .../exoplayer/ExoPlayerImplInternal.java | 12 ++--- .../exoplayer/MediaCodecTrackRenderer.java | 8 +-- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/CodecCounters.java b/library/src/main/java/com/google/android/exoplayer/CodecCounters.java index 7136ef2b1c..6b12b9c072 100644 --- a/library/src/main/java/com/google/android/exoplayer/CodecCounters.java +++ b/library/src/main/java/com/google/android/exoplayer/CodecCounters.java @@ -17,54 +17,41 @@ package com.google.android.exoplayer; /** * Maintains codec event counts, for debugging purposes only. + *

+ * 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(); } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index f1a0963969..193dba6646 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -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; @@ -100,7 +100,7 @@ import java.util.List; mediaClock = new MediaClock(); enabledRenderers = new ArrayList(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() { @@ -179,7 +179,7 @@ import java.util.List; Thread.currentThread().interrupt(); } } - internalPlayerThread.quit(); + internalPlaybackThread.quit(); } } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 9073fa93b5..14a83d2198 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -382,6 +382,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { while (feedInputBuffer()) {} } } + codecCounters.ensureUpdated(); } catch (IOException e) { throw new ExoPlaybackException(e); } @@ -403,7 +404,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { if (!sampleHolder.decodeOnly) { currentPositionUs = sampleHolder.timeUs; } - codecCounters.discardedSamplesCount++; } else if (result == SampleSource.FORMAT_READ) { onInputFormatChanged(formatHolder); } @@ -476,7 +476,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } if (result == SampleSource.NOTHING_READ) { - codecCounters.inputBufferWaitingForSampleCount++; return false; } if (result == SampleSource.DISCONTINUITY_READ) { @@ -505,7 +504,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); @@ -545,10 +543,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) { From 8ec88402617cf8ba26803bc5aeacf650956726df Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 Aug 2014 18:38:39 +0100 Subject: [PATCH 13/22] Minor cleanup. - Add constants class. Currently housing a single lonely variable, which is used generally throughout the library, and so no longer nicely fits into a specific class. - Rename a few other constants to add clear units. - Made minor tweak to ExoPlayer documentation. --- .../demo/full/player/DebugTrackRenderer.java | 4 +- .../google/android/exoplayer/ExoPlayer.java | 12 +++--- .../exoplayer/ExoPlayerImplInternal.java | 38 +++++++++---------- .../exoplayer/FrameworkSampleSource.java | 4 +- .../exoplayer/MediaCodecTrackRenderer.java | 2 +- .../android/exoplayer/SampleSource.java | 4 +- .../android/exoplayer/TrackRenderer.java | 14 +++---- .../google/android/exoplayer/chunk/Chunk.java | 5 ++- .../exoplayer/chunk/ChunkSampleSource.java | 6 +-- .../exoplayer/text/TextTrackRenderer.java | 2 +- .../exoplayer/upstream/ByteArrayDataSink.java | 3 +- .../upstream/ByteArrayDataSource.java | 7 ++-- .../exoplayer/upstream/DataSource.java | 8 ++-- .../exoplayer/upstream/DataSourceStream.java | 15 ++++---- .../android/exoplayer/upstream/DataSpec.java | 12 ++---- .../exoplayer/upstream/FileDataSource.java | 5 ++- .../exoplayer/upstream/HttpDataSource.java | 15 ++++---- .../exoplayer/upstream/TeeDataSource.java | 3 +- .../upstream/cache/CacheDataSource.java | 3 +- 19 files changed, 84 insertions(+), 78 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java index 498e087d12..8093bad814 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java @@ -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 diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java index 15288280aa..244a31eaf5 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java @@ -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. *

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

- * 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(); diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 193dba6646..9dc6228fa9 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -95,8 +95,8 @@ 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(rendererEnabledFlags.length); @@ -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); } @@ -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 { diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index bc1ee49a95..908664495f 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -72,7 +72,7 @@ public final class FrameworkSampleSource implements SampleSource { 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); } @@ -188,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; } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 14a83d2198..add1fa0015 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -340,7 +340,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()); } diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 56b20bf8a7..fc29ef1ad5 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -147,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(); diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index a8c08cf4f0..4abe763a7e 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -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; @@ -301,9 +301,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 +324,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(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java index 72a95232c5..a2afeb774e 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java @@ -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; @@ -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(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 40e1544bcc..860de3c179 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -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; @@ -383,14 +383,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; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 6f1e6f96ca..b281459d4d 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -230,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 diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java index 18b79ea547..d672dd95d7 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java @@ -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); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java index 937c9bc229..768d7061a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java @@ -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 diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java index 596bf764c3..eb269ef2dc 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.upstream; +import com.google.android.exoplayer.C; + import java.io.IOException; /** @@ -34,9 +36,9 @@ 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. For all other requests, the value returned will be + * equal to the request's {@link DataSpec#length}. */ public long open(DataSpec dataSpec) throws IOException; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java index 9060c88567..c786a05779 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java @@ -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; @@ -67,7 +68,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(); } @@ -99,11 +100,11 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream /** * Returns the length of the streamin bytes. * - * @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 +113,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; } /** @@ -144,7 +145,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 @@ -233,7 +234,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream } try { DataSpec loadDataSpec; - if (resolvedLength == DataSpec.LENGTH_UNBOUNDED) { + if (resolvedLength == C.LENGTH_UNBOUNDED) { loadDataSpec = dataSpec; resolvedLength = dataSource.open(loadDataSpec); if (resolvedLength > Integer.MAX_VALUE) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java index d9f5030d19..41f758be8a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java index 0ca8d3ea80..a08cacb982 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java @@ -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) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java index 374a648efa..7f9bdd4d26 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java @@ -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,16 @@ public class HttpDataSource implements DataSource { } long contentLength = getContentLength(connection); - dataLength = dataSpec.length == DataSpec.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; - if (dataLength == DataSpec.LENGTH_UNBOUNDED) { + dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; + if (dataLength == C.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), + new UnexpectedLengthException(C.LENGTH_UNBOUNDED, C.LENGTH_UNBOUNDED), dataSpec); } - if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED && contentLength != DataSpec.LENGTH_UNBOUNDED + if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED && contentLength != dataSpec.length) { // The DataSpec specified a length and we resolved a length from the response headers, but // the two lengths do not match. @@ -394,14 +395,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,7 +436,7 @@ public class HttpDataSource implements DataSource { } } } - if (contentLength == DataSpec.LENGTH_UNBOUNDED) { + if (contentLength == C.LENGTH_UNBOUNDED) { Log.w(TAG, "Unable to parse content length [" + contentLengthHeader + "] [" + contentRangeHeader + "]"); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java index b54db19843..6d05f8c344 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java @@ -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) { // 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); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java index 1f82252540..379b63fa53 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java @@ -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; @@ -124,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; From 005e98fc34f7fc24d00673f930f29b9b21283983 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 Aug 2014 19:42:04 +0100 Subject: [PATCH 14/22] Simplify parsing of encryption data + support SENC boxes. Issue: #4 --- .../android/exoplayer/parser/mp4/Atom.java | 1 + .../parser/mp4/FragmentedMp4Extractor.java | 135 +++++++++--------- .../exoplayer/parser/mp4/TrackFragment.java | 23 ++- 3 files changed, 79 insertions(+), 80 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java index 716ca458bf..b69a6dbd40 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java @@ -54,6 +54,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; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 7a990ee2a7..936deb09e0 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -93,7 +93,7 @@ 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_ENCRYPTION_DATA = 2; private static final int STATE_READING_SAMPLE = 3; // Atom data offsets @@ -130,6 +130,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); } @@ -162,8 +163,6 @@ public final class FragmentedMp4Extractor { private int atomType; private int atomSize; private ParsableByteArray atomData; - private ParsableByteArray cencAuxiliaryData; - private int cencAuxiliaryBytesRead; private int pendingSeekTimeMs; private int sampleIndex; @@ -273,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); @@ -330,9 +329,6 @@ public final class FragmentedMp4Extractor { rootAtomBytesRead = 0; } break; - case STATE_READING_CENC_AUXILIARY_DATA: - cencAuxiliaryBytesRead = 0; - break; } parserState = state; } @@ -354,12 +350,9 @@ 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); } return 0; @@ -631,7 +624,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 audioSpecificConfig = CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); sampleRate = audioSpecificConfig.first; @@ -752,7 +745,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,26 +782,38 @@ public final class FragmentedMp4Extractor { */ 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); - } LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).getData()); + LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.getData()); out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex); LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out); + + TrackEncryptionBox trackEncryptionBox = + track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex]; + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + if (saiz != null) { + parseSaiz(trackEncryptionBox, saiz.getData(), out); + } + + LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + if (senc != null) { + parseSenc(senc.getData(), out); + } + LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid); if (uuid != null) { parseUuid(uuid.getData(), out); } } - 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); @@ -818,19 +823,20 @@ public final class FragmentedMp4Extractor { int defaultSampleInfoSize = saiz.readUnsignedByte(); int sampleCount = saiz.readUnsignedIntToInt(); int totalSize = 0; - int[] sampleInfoSizes = new int[sampleCount]; + boolean[] sampleHasSubsampleEncryptionTable = new boolean[sampleCount]; if (defaultSampleInfoSize == 0) { 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(sampleHasSubsampleEncryptionTable, subsampleEncryption); } - out.setAuxiliarySampleInfoTables(totalSize, sampleInfoSizes); + out.setSampleEncryptionData(sampleHasSubsampleEncryptionTable, + new ParsableByteArray(totalSize), true); } /** @@ -942,13 +948,8 @@ public final class FragmentedMp4Extractor { } 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; } @@ -966,9 +967,19 @@ public final class FragmentedMp4Extractor { 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) { @@ -977,15 +988,19 @@ 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(); + boolean[] sampleHasSubsampleEncryptionTable = new boolean[sampleCount]; + Arrays.fill(sampleHasSubsampleEncryptionTable, subsampleEncryption); + + int sampleEncryptionDataLength = senc.length() - senc.getPosition(); ParsableByteArray sampleEncryptionData = new ParsableByteArray(sampleEncryptionDataLength); - uuid.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionData.length()); - out.setSmoothStreamingSampleEncryptionData(sampleEncryptionData, subsampleEncryption); + senc.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionDataLength); + + out.setSampleEncryptionData(sampleHasSubsampleEncryptionTable, sampleEncryptionData, false); } /** @@ -1044,17 +1059,14 @@ 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) { + ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; + int sampleEncryptionDataLength = sampleEncryptionData.length(); + if (inputStream.getAvailableByteCount() < sampleEncryptionDataLength) { return RESULT_NEED_MORE_DATA; } + inputStream.read(sampleEncryptionData.getData(), 0, sampleEncryptionDataLength); + fragmentRun.sampleEncryptionDataNeedsFill = false; enterState(STATE_READING_SAMPLE); return 0; } @@ -1093,15 +1105,12 @@ public final class FragmentedMp4Extractor { } private int skipSample(NonBlockingInputStream inputStream, int sampleSize) { - ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData - : fragmentRun.smoothStreamingSampleEncryptionData; + ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; if (sampleEncryptionData != null) { TrackEncryptionBox encryptionBox = track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; int vectorSize = encryptionBox.initializationVectorSize; - boolean subsampleEncryption = cencAuxiliaryData != null - ? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize - : fragmentRun.smoothStreamingUsesSubsampleEncryption; + boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex]; sampleEncryptionData.skip(vectorSize); int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1; if (subsampleEncryption) { @@ -1128,13 +1137,11 @@ public final class FragmentedMp4Extractor { out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; lastSyncSampleIndex = sampleIndex; } - if (out.allowDataBufferReplacement - && (out.data == null || out.data.capacity() < sampleSize)) { + if (out.allowDataBufferReplacement && (out.data == null || out.data.capacity() < sampleSize)) { outputData = ByteBuffer.allocate(sampleSize); out.data = outputData; } - ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData - : fragmentRun.smoothStreamingSampleEncryptionData; + ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; if (sampleEncryptionData != null) { readSampleEncryptionData(sampleEncryptionData, out); } @@ -1173,9 +1180,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) { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java index 98f33968ca..47a5a23880 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java @@ -27,12 +27,10 @@ package com.google.android.exoplayer.parser.mp4; public int[] sampleDecodingTimeTable; public int[] sampleCompositionTimeOffsetTable; public boolean[] sampleIsSyncFrameTable; + public boolean[] sampleHasSubsampleEncryptionTable; - public int auxiliarySampleInfoTotalSize; - public int[] auxiliarySampleInfoSizeTable; - - public boolean smoothStreamingUsesSubsampleEncryption; - public ParsableByteArray smoothStreamingSampleEncryptionData; + public ParsableByteArray sampleEncryptionData; + public boolean sampleEncryptionDataNeedsFill; public void setSampleDescriptionIndex(int sampleDescriptionIndex) { this.sampleDescriptionIndex = sampleDescriptionIndex; @@ -47,16 +45,11 @@ package com.google.android.exoplayer.parser.mp4; this.length = sampleSizeTable.length; } - public void setAuxiliarySampleInfoTables(int totalAuxiliarySampleInfoSize, - int[] auxiliarySampleInfoSizeTable) { - this.auxiliarySampleInfoTotalSize = totalAuxiliarySampleInfoSize; - this.auxiliarySampleInfoSizeTable = auxiliarySampleInfoSizeTable; - } - - public void setSmoothStreamingSampleEncryptionData(ParsableByteArray data, - boolean usesSubsampleEncryption) { - this.smoothStreamingSampleEncryptionData = data; - this.smoothStreamingUsesSubsampleEncryption = usesSubsampleEncryption; + public void setSampleEncryptionData(boolean[] sampleHasSubsampleEncryptionTable, + ParsableByteArray sampleEncryptionData, boolean sampleEncryptionDataNeedsFill) { + this.sampleHasSubsampleEncryptionTable = sampleHasSubsampleEncryptionTable; + this.sampleEncryptionData = sampleEncryptionData; + this.sampleEncryptionDataNeedsFill = sampleEncryptionDataNeedsFill; } public int getSamplePresentationTime(int index) { From 4a745b1cd84172e8d58c55ca561dd22ec5d339c3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Aug 2014 13:55:38 +0100 Subject: [PATCH 15/22] Optimize out quite a few allocations in FragmentedMp4Parser. --- .../google/android/exoplayer/chunk/Chunk.java | 2 +- .../android/exoplayer/parser/mp4/Atom.java | 18 +- .../parser/mp4/CodecSpecificDataUtil.java | 2 +- .../parser/mp4/FragmentedMp4Extractor.java | 156 ++++++++---------- .../parser/mp4/ParsableByteArray.java | 7 +- .../parser/mp4/TrackEncryptionBox.java | 2 +- .../exoplayer/parser/mp4/TrackFragment.java | 127 ++++++++++++-- 7 files changed, 187 insertions(+), 127 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java index a2afeb774e..6724e283fb 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java @@ -52,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}. diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java index b69a6dbd40..fbdccc2d67 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java @@ -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; @@ -64,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 { @@ -91,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; @@ -101,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; @@ -110,10 +106,6 @@ import java.util.List; return null; } - public List getChildren() { - return children; - } - } } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java index e441670cf6..851c4925b6 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java @@ -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}; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 936deb09e0..936bd565d0 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -110,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); @@ -154,8 +153,10 @@ public final class FragmentedMp4Extractor { // Parser state private final ParsableByteArray atomHeader; + private final byte[] extendedTypeScratch; private final Stack containerAtoms; private final Stack containerAtomEndPoints; + private final TrackFragment fragmentRun; private int parserState; private int atomBytesRead; @@ -175,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); } @@ -190,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(); containerAtomEndPoints = new Stack(); + fragmentRun = new TrackFragment(); psshData = new HashMap(); } @@ -335,7 +335,7 @@ public final class FragmentedMp4Extractor { 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; } @@ -365,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 { @@ -379,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); } @@ -411,7 +411,7 @@ public final class FragmentedMp4Extractor { if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(leaf); } else if (leaf.type == Atom.TYPE_sidx) { - segmentIndex = parseSidx(leaf.getData()); + segmentIndex = parseSidx(leaf.data); return RESULT_READ_INDEX; } return 0; @@ -430,11 +430,12 @@ public final class FragmentedMp4Extractor { } private void onMoovContainerAtomRead(ContainerAtom moov) { - List moovChildren = moov.getChildren(); - for (int i = 0; i < moovChildren.size(); i++) { + List 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(); @@ -444,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; @@ -484,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 header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).getData()); + Pair 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 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); } @@ -667,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, @@ -759,55 +760,41 @@ 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) { + 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.getData(), out); + parseSaiz(trackEncryptionBox, saiz.data, out); } LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); if (senc != null) { - parseSenc(senc.getData(), out); + parseSenc(senc.data, out); } LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid); if (uuid != null) { - parseUuid(uuid.getData(), out); + parseUuid(uuid.data, out, extendedTypeScratch); } } @@ -821,10 +808,15 @@ 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; - boolean[] sampleHasSubsampleEncryptionTable = new boolean[sampleCount]; if (defaultSampleInfoSize == 0) { + boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable; for (int i = 0; i < sampleCount; i++) { int sampleInfoSize = saiz.readUnsignedByte(); totalSize += sampleInfoSize; @@ -833,10 +825,9 @@ public final class FragmentedMp4Extractor { } else { boolean subsampleEncryption = defaultSampleInfoSize > vectorSize; totalSize += defaultSampleInfoSize * sampleCount; - Arrays.fill(sampleHasSubsampleEncryptionTable, subsampleEncryption); + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); } - out.setSampleEncryptionData(sampleHasSubsampleEncryptionTable, - new ParsableByteArray(totalSize), true); + out.initEncryptionData(totalSize); } /** @@ -895,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); } @@ -915,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; @@ -933,18 +924,15 @@ 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; @@ -952,18 +940,15 @@ public final class FragmentedMp4Extractor { && (!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; } @@ -993,14 +978,9 @@ public final class FragmentedMp4Extractor { throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length); } - boolean[] sampleHasSubsampleEncryptionTable = new boolean[sampleCount]; - Arrays.fill(sampleHasSubsampleEncryptionTable, subsampleEncryption); - - int sampleEncryptionDataLength = senc.length() - senc.getPosition(); - ParsableByteArray sampleEncryptionData = new ParsableByteArray(sampleEncryptionDataLength); - senc.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionDataLength); - - out.setSampleEncryptionData(sampleHasSubsampleEncryptionTable, sampleEncryptionData, false); + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + out.initEncryptionData(senc.length() - senc.getPosition()); + out.fillEncryptionData(senc); } /** @@ -1060,13 +1040,10 @@ public final class FragmentedMp4Extractor { } private int readEncryptionData(NonBlockingInputStream inputStream) { - ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; - int sampleEncryptionDataLength = sampleEncryptionData.length(); - if (inputStream.getAvailableByteCount() < sampleEncryptionDataLength) { + boolean success = fragmentRun.fillEncryptionData(inputStream); + if (!success) { return RESULT_NEED_MORE_DATA; } - inputStream.read(sampleEncryptionData.getData(), 0, sampleEncryptionDataLength); - fragmentRun.sampleEncryptionDataNeedsFill = false; enterState(STATE_READING_SAMPLE); return 0; } @@ -1105,8 +1082,8 @@ public final class FragmentedMp4Extractor { } private int skipSample(NonBlockingInputStream inputStream, int sampleSize) { - ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; - if (sampleEncryptionData != null) { + if (fragmentRun.definesEncryptionData) { + ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; TrackEncryptionBox encryptionBox = track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; int vectorSize = encryptionBox.initializationVectorSize; @@ -1141,9 +1118,8 @@ public final class FragmentedMp4Extractor { outputData = ByteBuffer.allocate(sampleSize); out.data = outputData; } - ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData; - if (sampleEncryptionData != null) { - readSampleEncryptionData(sampleEncryptionData, out); + if (fragmentRun.definesEncryptionData) { + readSampleEncryptionData(fragmentRun.sampleEncryptionData, out); } if (outputData == null) { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java index 13027f0174..bf4472d5a7 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java index 2cc899ef71..6a300e7230 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java @@ -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. diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java index 47a5a23880..e2e08225b2 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java @@ -15,41 +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 void setSampleDescriptionIndex(int sampleDescriptionIndex) { - this.sampleDescriptionIndex = sampleDescriptionIndex; + /** + * Resets the fragment. + *

+ * 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. + *

+ * 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 setSampleEncryptionData(boolean[] sampleHasSubsampleEncryptionTable, - ParsableByteArray sampleEncryptionData, boolean sampleEncryptionDataNeedsFill) { - this.sampleHasSubsampleEncryptionTable = sampleHasSubsampleEncryptionTable; - this.sampleEncryptionData = sampleEncryptionData; - this.sampleEncryptionDataNeedsFill = sampleEncryptionDataNeedsFill; + /** + * Configures the fragment to be one that defines encryption data of the specified length. + *

+ * {@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; + } + + /** + * 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) { From e0a29c841ebcfb887c5562e4f12f985799e61b4a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Aug 2014 14:12:05 +0100 Subject: [PATCH 16/22] Don't release AudioTrack unless we have to. --- .../exoplayer/MediaCodecAudioTrackRenderer.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index c02e8a9135..798282dd91 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -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); From 58e9e61688002a7d75d2e3841a1b5d745559a4e3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Aug 2014 14:13:43 +0100 Subject: [PATCH 17/22] Remove unnecessary SuppressWarning annotations. --- .../google/android/exoplayer/MediaCodecTrackRenderer.java | 8 ++++++-- .../java/com/google/android/exoplayer/TrackRenderer.java | 1 - .../google/android/exoplayer/text/TextTrackRenderer.java | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index add1fa0015..6f7262f79a 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -187,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. @@ -628,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; diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index 4abe763a7e..f27433d06e 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -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(); diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index b281459d4d..1fd213eda1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -280,7 +280,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } } - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { From 5cfa9adaccedb78834eec094c221fe543d954e22 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Aug 2014 14:16:28 +0100 Subject: [PATCH 18/22] Add missing C file. --- .../java/com/google/android/exoplayer/C.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/C.java diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java new file mode 100644 index 0000000000..c8cd9fe586 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -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() {} + +} From af6e144adc5bb4dbf7d2022e56c01baf0879396e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Aug 2014 21:35:13 +0100 Subject: [PATCH 19/22] Fix bug introduced supporting self-contained media chunks. The equals check we perform needs to ignore the max dimensions. This tended to work in practice because formats would be the same object, but in the case where different format objects are used, things can break. --- .../google/android/exoplayer/MediaFormat.java | 25 ++++++++++++++----- .../exoplayer/chunk/ChunkSampleSource.java | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index f53defdf93..3188e36db0 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -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++) { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 860de3c179..069f5c69dc 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -319,7 +319,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(); From 4e7b333aee2af26376230c054eee0d25c443134d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 14 Aug 2014 15:44:09 +0100 Subject: [PATCH 20/22] Support chunked requests. --- .../exoplayer/chunk/ChunkSampleSource.java | 39 +++++++------ .../exoplayer/upstream/Allocation.java | 22 ++++++++ .../exoplayer/upstream/BufferPool.java | 40 ++++++++++++- .../exoplayer/upstream/DataSource.java | 5 +- .../exoplayer/upstream/DataSourceStream.java | 56 +++++++++++++------ .../exoplayer/upstream/HttpDataSource.java | 26 +++------ .../exoplayer/upstream/TeeDataSource.java | 2 +- .../upstream/cache/CacheDataSink.java | 4 ++ 8 files changed, 135 insertions(+), 59 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 069f5c69dc..980de4ec23 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -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 @@ -409,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) { @@ -424,7 +428,6 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } - notifyLoadCompleted(); updateLoadControl(); } } @@ -432,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 { @@ -677,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); } }); } @@ -760,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); } }); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java b/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java index 2b7619595b..b3fb38921a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java @@ -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. + *

+ * 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. + *

+ * 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. * diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java index 979dc39e46..a7d847d5a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java @@ -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}. + *

+ * 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; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java index eb269ef2dc..624e42a111 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java @@ -37,8 +37,9 @@ public interface DataSource { * @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 C#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}. + * 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; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java index c786a05779..dc5227e426 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java @@ -40,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; @@ -58,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. @@ -98,7 +100,8 @@ 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 C#LENGTH_UNBOUNDED} if the length has * yet to be determined. @@ -124,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) { @@ -192,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); @@ -204,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; @@ -232,23 +235,32 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream // The load was canceled, or is already complete. return; } + try { DataSpec loadDataSpec; - if (resolvedLength == C.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); @@ -257,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)); @@ -283,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; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java index 7f9bdd4d26..f9d3bf8f1a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java @@ -260,13 +260,6 @@ public class HttpDataSource implements DataSource { long contentLength = getContentLength(connection); dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; - if (dataLength == C.LENGTH_UNBOUNDED) { - // The DataSpec specified unbounded length and we failed to resolve a length from the - // response headers. - throw new HttpDataSourceException( - new UnexpectedLengthException(C.LENGTH_UNBOUNDED, C.LENGTH_UNBOUNDED), - dataSpec); - } if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED && contentLength != dataSpec.length) { @@ -306,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); } @@ -365,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}. + *

+ * 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 { @@ -436,10 +430,6 @@ public class HttpDataSource implements DataSource { } } } - if (contentLength == C.LENGTH_UNBOUNDED) { - Log.w(TAG, "Unable to parse content length [" + contentLengthHeader + "] [" + - contentRangeHeader + "]"); - } return contentLength; } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java index 6d05f8c344..cbb571f308 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java @@ -40,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 == C.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); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java index 02177f7d93..942a29f0c7 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java @@ -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; From 5b9c92cb83f0b72ccae7f80ad17ccde9a72c212b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 14 Aug 2014 15:45:33 +0100 Subject: [PATCH 21/22] Bump version to 1.0.12. --- demo/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer/ExoPlayerLibraryInfo.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 330adc6d2b..98ed2f18a9 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 4661568d1e..06973db73b 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -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 = 1000011; + public static final int VERSION_INT = 1000012; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} From cc04fd1e76b988ac696271501355b71875ec13f0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 14 Aug 2014 16:19:30 +0100 Subject: [PATCH 22/22] Fix the build. --- .../exoplayer/demo/full/EventLogger.java | 4 ++-- .../exoplayer/demo/full/player/DemoPlayer.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java index 2098997297..722e21c45e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java @@ -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 + ", " + diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index 63a2557541..bdaa2e5a73 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -123,8 +123,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi void onDroppedFrames(int count, long elapsed); 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); } /** @@ -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. }