From d4e35358a1737f5e5d7261735cdaf1b89e67c463 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:09:52 +0100 Subject: [PATCH 001/110] Fix bad assertion. It doesn't hold for TTML subtitle chunks. --- .../java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2793496b53..a4d05cacd7 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 @@ -96,7 +96,7 @@ public final class Mp4MediaChunk extends MediaChunk { prepared = true; } if (prepared) { - mediaFormat = Assertions.checkNotNull(extractor.getFormat()); + mediaFormat = extractor.getFormat(); psshInfo = extractor.getPsshInfo(); } } From bf5ee6ff230a60a0c7c716c902712d24ea08aaa0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:19:26 +0100 Subject: [PATCH 002/110] 1. Parse live attributes from SmoothStreaming manifest. 2. Common interface for manifest parsers. - This effectively moves the common interface from the Fetcher level (i.e. ManifestFetcher) to the Parser level (i.e. ManifestParser). - The motivation here is to allow the implementation of components that can work with a generic ManifestParser implementation. --- .../full/player/DashVodRendererBuilder.java | 7 +- .../SmoothStreamingRendererBuilder.java | 16 ++- .../demo/simple/DashVodRendererBuilder.java | 7 +- .../SmoothStreamingRendererBuilder.java | 19 +-- .../java/com/google/android/exoplayer/C.java | 5 + .../MediaPresentationDescriptionFetcher.java | 64 --------- .../MediaPresentationDescriptionParser.java | 125 +++--------------- .../android/exoplayer/dash/mpd/RangedUri.java | 21 +-- .../parser/webm/DefaultEbmlReader.java | 3 +- .../SmoothStreamingChunkSource.java | 14 +- .../SmoothStreamingManifest.java | 78 +++++++---- .../SmoothStreamingManifestFetcher.java | 63 --------- .../SmoothStreamingManifestParser.java | 108 ++++++++------- .../smoothstreaming/SmoothStreamingUtil.java | 86 ++++++++++++ .../exoplayer/text/TextTrackRenderer.java | 3 +- .../exoplayer/util/ManifestFetcher.java | 36 ++--- .../exoplayer/util/ManifestParser.java | 47 +++++++ .../google/android/exoplayer/util/Util.java | 114 ++++++++++++++++ 18 files changed, 444 insertions(+), 372 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java create mode 100644 library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java create mode 100644 library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 4ee13a75fc..966ce7a43b 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -31,7 +31,7 @@ import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; -import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.demo.DemoUtil; @@ -44,6 +44,7 @@ import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; @@ -94,7 +95,9 @@ public class DashVodRendererBuilder implements RendererBuilder, public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; - MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this); + MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); + ManifestFetcher mpdFetcher = + new ManifestFetcher(parser, this); mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 5a4e9a58cb..af806e9fe7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -35,13 +35,14 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; -import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser; import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.text.ttml.TtmlParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.Util; @@ -87,8 +88,11 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; - SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this); - mpdFetcher.execute(url + "/Manifest", contentId); + + SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); + ManifestFetcher manifestFetcher = + new ManifestFetcher(parser, this); + manifestFetcher.execute(url + "/Manifest", contentId); } @Override @@ -151,7 +155,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex, videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, @@ -178,7 +182,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, for (int i = 0; i < manifest.streamElements.length; i++) { if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) { audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name; - audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(url, manifest, + audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(manifest, i, new int[] {0}, audioDataSource, audioFormatEvaluator); audioStreamElementCount++; } @@ -208,7 +212,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, for (int i = 0; i < manifest.streamElements.length; i++) { if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) { textTrackNames[textStreamElementCount] = manifest.streamElements[i].language; - textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(url, manifest, + textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifest, i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator); textStreamElementCount++; } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index a253de873e..cc994543b9 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; -import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder; @@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; @@ -74,7 +75,9 @@ import java.util.ArrayList; @Override public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; - MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this); + MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); + ManifestFetcher mpdFetcher = + new ManifestFetcher(parser, this); mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index 0b92810073..efde2de096 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -31,11 +31,12 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; -import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; @@ -71,8 +72,10 @@ import java.util.ArrayList; @Override public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; - SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this); - mpdFetcher.execute(url + "/Manifest", contentId); + SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); + ManifestFetcher manifestFetcher = + new ManifestFetcher(parser, this); + manifestFetcher.execute(url + "/Manifest", contentId); } @Override @@ -116,9 +119,8 @@ import java.util.ArrayList; // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, - videoStreamElementIndex, videoTrackIndices, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter)); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex, + videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, @@ -126,9 +128,8 @@ import java.util.ArrayList; // Build the audio renderer. DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest, - audioStreamElementIndex, new int[] {0}, audioDataSource, - new FormatEvaluator.FixedEvaluator()); + ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifest, audioStreamElementIndex, + new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator()); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index c8cd9fe586..f0f2a57fec 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -25,6 +25,11 @@ public final class C { */ public static final int LENGTH_UNBOUNDED = -1; + /** + * The name of the UTF-8 charset. + */ + public static final String UTF8_NAME = "UTF-8"; + private C() {} } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java deleted file mode 100644 index 45885cfc90..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.dash.mpd; - -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.util.ManifestFetcher; - -import android.net.Uri; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A concrete implementation of {@link ManifestFetcher} for loading DASH manifests. - *

- * This class is provided for convenience, however it is expected that most applications will - * contain their own mechanisms for making asynchronous network requests and parsing the response. - * In such cases it is recommended that application developers use their existing solution rather - * than this one. - */ -public final class MediaPresentationDescriptionFetcher extends - ManifestFetcher { - - private final MediaPresentationDescriptionParser parser; - - /** - * @param callback The callback to provide with the parsed manifest (or error). - */ - public MediaPresentationDescriptionFetcher( - ManifestCallback callback) { - super(callback); - parser = new MediaPresentationDescriptionParser(); - } - - /** - * @param callback The callback to provide with the parsed manifest (or error). - * @param timeoutMillis The timeout in milliseconds for the connection used to load the data. - */ - public MediaPresentationDescriptionFetcher( - ManifestCallback callback, int timeoutMillis) { - super(callback, timeoutMillis); - parser = new MediaPresentationDescriptionParser(); - } - - @Override - protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, - String contentId, Uri baseUrl) throws IOException, ParserException { - return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl); - } - -} 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 0011c5d225..606f91a4ef 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 @@ -22,7 +22,9 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ManifestParser; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; import android.net.Uri; import android.text.TextUtils; @@ -34,29 +36,15 @@ import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.InputStream; -import java.math.BigDecimal; import java.text.ParseException; import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; import java.util.List; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * A parser of media presentation description files. */ -public class MediaPresentationDescriptionParser extends DefaultHandler { - - // Note: Does not support the date part of ISO 8601 - private static final Pattern DURATION = - Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$"); - - private static final Pattern DATE_TIME_PATTERN = - Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" - + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" - + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); +public class MediaPresentationDescriptionParser extends DefaultHandler + implements ManifestParser { private final XmlPullParserFactory xmlParserFactory; @@ -70,19 +58,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { // MPD parsing. - /** - * Parses a manifest from the provided {@link InputStream}. - * - * @param inputStream The stream from which to parse the manifest. - * @param inputEncoding The encoding of the input. - * @param contentId The content id of the media. - * @param baseUrl The url that any relative urls defined within the manifest are relative to. - * @return The parsed manifest. - * @throws IOException If a problem occurred reading from the stream. - * @throws ParserException If a problem occurred parsing the xml as a DASH mpd. - */ - public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream, - String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException { + @Override + public MediaPresentationDescription parse(InputStream inputStream, String inputEncoding, + String contentId, Uri baseUrl) throws IOException, ParserException { try { XmlPullParser xpp = xmlParserFactory.newPullParser(); xpp.setInput(inputStream, inputEncoding); @@ -102,12 +80,13 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException { long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1); - long durationMs = parseDurationMs(xpp, "mediaPresentationDuration"); - long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime"); + long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1); + long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1); String typeString = xpp.getAttributeValue(null, "type"); boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false; - long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1; - long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1; + long minUpdateTimeMs = (dynamic) ? parseDuration(xpp, "minimumUpdatePeriod", -1) : -1; + long timeShiftBufferDepthMs = (dynamic) ? parseDuration(xpp, "timeShiftBufferDepth", -1) + : -1; UtcTimingElement utcTiming = null; List periods = new ArrayList(); @@ -135,8 +114,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); - long startMs = parseDurationMs(xpp, "start", 0); - long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs); + long startMs = parseDuration(xpp, "start", 0); + long durationMs = parseDuration(xpp, "duration", mpdDurationMs); SegmentBase segmentBase = null; List adaptationSets = new ArrayList(); do { @@ -450,85 +429,25 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName()); } - private static long parseDurationMs(XmlPullParser xpp, String name) { - return parseDurationMs(xpp, name, -1); + private static long parseDuration(XmlPullParser xpp, String name, long defaultValue) { + String value = xpp.getAttributeValue(null, name); + if (value == null) { + return defaultValue; + } else { + return Util.parseXsDuration(value); + } } private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) throws ParseException { String value = xpp.getAttributeValue(null, name); - if (value == null) { return defaultValue; } else { - return parseDateTime(value); + return Util.parseXsDateTime(value); } } - // VisibleForTesting - static long parseDateTime(String value) throws ParseException { - Matcher matcher = DATE_TIME_PATTERN.matcher(value); - if (!matcher.matches()) { - throw new ParseException("Invalid date/time format: " + value, 0); - } - - int timezoneShift; - if (matcher.group(9) == null) { - // No time zone specified. - timezoneShift = 0; - } else if (matcher.group(9).equalsIgnoreCase("Z")) { - timezoneShift = 0; - } else { - timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60 - + Integer.valueOf(matcher.group(13)))); - if (matcher.group(11).equals("-")) { - timezoneShift *= -1; - } - } - - Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - - dateTime.clear(); - // Note: The month value is 0-based, hence the -1 on group(2) - dateTime.set(Integer.valueOf(matcher.group(1)), - Integer.valueOf(matcher.group(2)) - 1, - Integer.valueOf(matcher.group(3)), - Integer.valueOf(matcher.group(4)), - Integer.valueOf(matcher.group(5)), - Integer.valueOf(matcher.group(6))); - if (!TextUtils.isEmpty(matcher.group(8))) { - final BigDecimal bd = new BigDecimal("0." + matcher.group(8)); - // we care only for milliseconds, so movePointRight(3) - dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue()); - } - - long time = dateTime.getTimeInMillis(); - if (timezoneShift != 0) { - time -= timezoneShift * 60000; - } - - return time; - } - - private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) { - String value = xpp.getAttributeValue(null, name); - if (value != null) { - Matcher matcher = DURATION.matcher(value); - if (matcher.matches()) { - String hours = matcher.group(2); - double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0; - String minutes = matcher.group(4); - durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0; - String seconds = matcher.group(6); - durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0; - return (long) (durationSeconds * 1000); - } else { - return (long) (Double.parseDouble(value) * 3600 * 1000); - } - } - return defaultValue; - } - protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) throws XmlPullParserException, IOException { xpp.next(); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java index cd18f85599..2ce5ad3092 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -47,15 +48,10 @@ public final class RangedUri { /** * Constructs an ranged uri. *

- * The uri is built according to the following rules: - *

    - *
  • If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is - * ignored and the url consists solely of {@code stringUri}. - *
  • If {@code stringUri} is null, then the url consists solely of {@code baseUrl}. - *
  • Otherwise, the url consists of the concatenation of {@code baseUri} and {@code stringUri}. - *
+ * See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and + * {@code stringUri} are merged. * - * @param baseUri An uri that can form the base of the uri defined by the instance. + * @param baseUri A uri that can form the base of the uri defined by the instance. * @param stringUri A relative or absolute uri in string form. * @param start The (zero based) index of the first byte of the range. * @param length The length of the range, or -1 to indicate that the range is unbounded. @@ -74,14 +70,7 @@ public final class RangedUri { * @return The {@link Uri} represented by the instance. */ public Uri getUri() { - if (stringUri == null) { - return baseUri; - } - Uri uri = Uri.parse(stringUri); - if (!uri.isAbsolute() && baseUri != null) { - uri = Uri.withAppendedPath(baseUri, stringUri); - } - return uri; + return Util.getMergedUri(baseUri, stringUri); } /** 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 55eca63de6..daea459e93 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.parser.webm; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; @@ -210,7 +211,7 @@ import java.util.Stack; if (stringResult != READ_RESULT_CONTINUE) { return stringResult; } - String stringValue = new String(stringBytes, Charset.forName("UTF-8")); + String stringValue = new String(stringBytes, Charset.forName(C.UTF8_NAME)); stringBytes = null; eventHandler.onStringElement(elementId, stringValue); prepareForNextElement(); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 918ddb4f90..a6af466727 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -53,7 +53,6 @@ public class SmoothStreamingChunkSource implements ChunkSource { private static final int INITIALIZATION_VECTOR_SIZE = 8; - private final String baseUrl; private final StreamElement streamElement; private final TrackInfo trackInfo; private final DataSource dataSource; @@ -67,7 +66,6 @@ public class SmoothStreamingChunkSource implements ChunkSource { private final SmoothStreamingFormat[] formats; /** - * @param baseUrl The base URL for the streams. * @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}. * @param streamElementIndex The index of the stream element in the manifest to be provided by * the source. @@ -76,10 +74,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { * @param dataSource A {@link DataSource} suitable for loading the media data. * @param formatEvaluator Selects from the available formats. */ - public SmoothStreamingChunkSource(String baseUrl, SmoothStreamingManifest manifest, - int streamElementIndex, int[] trackIndices, DataSource dataSource, - FormatEvaluator formatEvaluator) { - this.baseUrl = baseUrl; + public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex, + int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { this.streamElement = manifest.streamElements[streamElementIndex]; this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.getDurationUs()); this.dataSource = dataSource; @@ -113,7 +109,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { : Track.TYPE_AUDIO; FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); - extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat, + extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, mediaFormat, trackEncryptionBoxes)); if (protectionElement != null) { extractor.putPsshInfo(protectionElement.uuid, protectionElement.data); @@ -183,9 +179,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { } boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1; - String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex, - nextChunkIndex); - Uri uri = Uri.parse(baseUrl + '/' + requestUrl); + Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, nextChunkIndex); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex, isLastChunk, streamElement.getStartTimeUs(nextChunkIndex), diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index d6a739ee1b..28f046816e 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -15,9 +15,13 @@ */ package com.google.android.exoplayer.smoothstreaming; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; +import android.net.Uri; + +import java.util.List; import java.util.UUID; /** @@ -30,32 +34,48 @@ public class SmoothStreamingManifest { public final int majorVersion; public final int minorVersion; - public final long timeScale; + public final long timescale; public final int lookAheadCount; + public final boolean isLive; public final ProtectionElement protectionElement; public final StreamElement[] streamElements; private final long duration; + private final long dvrWindowLength; - public SmoothStreamingManifest(int majorVersion, int minorVersion, long timeScale, long duration, - int lookAheadCount, ProtectionElement protectionElement, StreamElement[] streamElements) { + public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration, + long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, + StreamElement[] streamElements) { this.majorVersion = majorVersion; this.minorVersion = minorVersion; - this.timeScale = timeScale; + this.timescale = timescale; this.duration = duration; + this.dvrWindowLength = dvrWindowLength; this.lookAheadCount = lookAheadCount; + this.isLive = isLive; this.protectionElement = protectionElement; this.streamElements = streamElements; } /** * Gets the duration of the media. + *

+ * For a live presentation the duration may be an approximation of the eventual final duration, + * or 0 if an approximate duration is not known. * - * - * @return The duration of the media, in microseconds. + * @return The duration of the media in microseconds. */ public long getDurationUs() { - return (duration * 1000000L) / timeScale; + return (duration * 1000000L) / timescale; + } + + /** + * Gets the DVR window length, or 0 if no window length was specified. + * + * @return The duration of the DVR window in microseconds, or 0 if no window length was specified. + */ + public long getDvrWindowLengthUs() { + return (dvrWindowLength * 1000000L) / timescale; } /** @@ -155,10 +175,9 @@ public class SmoothStreamingManifest { public final int type; public final String subType; - public final long timeScale; + public final long timescale; public final String name; public final int qualityLevels; - public final String url; public final int maxWidth; public final int maxHeight; public final int displayWidth; @@ -167,24 +186,29 @@ public class SmoothStreamingManifest { public final TrackElement[] tracks; public final int chunkCount; - private final long[] chunkStartTimes; + private final Uri baseUri; + private final String chunkTemplate; - public StreamElement(int type, String subType, long timeScale, String name, - int qualityLevels, String url, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, TrackElement[] tracks, long[] chunkStartTimes) { + private final List chunkStartTimes; + + public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType, + long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, + int displayWidth, int displayHeight, String language, TrackElement[] tracks, + List chunkStartTimes) { + this.baseUri = baseUri; + this.chunkTemplate = chunkTemplate; this.type = type; this.subType = subType; - this.timeScale = timeScale; + this.timescale = timescale; this.name = name; this.qualityLevels = qualityLevels; - this.url = url; this.maxWidth = maxWidth; this.maxHeight = maxHeight; this.displayWidth = displayWidth; this.displayHeight = displayHeight; this.language = language; this.tracks = tracks; - this.chunkCount = chunkStartTimes.length; + this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; } @@ -195,7 +219,7 @@ public class SmoothStreamingManifest { * @return The index of the corresponding chunk. */ public int getChunkIndex(long timeUs) { - return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true); + return Util.binarySearchFloor(chunkStartTimes, (timeUs * timescale) / 1000000L, true, true); } /** @@ -205,22 +229,24 @@ public class SmoothStreamingManifest { * @return The start time of the chunk, in microseconds. */ public long getStartTimeUs(int chunkIndex) { - return (chunkStartTimes[chunkIndex] * 1000000L) / timeScale; + return (chunkStartTimes.get(chunkIndex) * 1000000L) / timescale; } /** - * Builds a URL for requesting the specified chunk of the specified track. + * Builds a uri for requesting the specified chunk of the specified track. * * @param track The index of the track for which to build the URL. * @param chunkIndex The index of the chunk for which to build the URL. - * @return The request URL. + * @return The request uri. */ - public String buildRequestUrl(int track, int chunkIndex) { - assert (tracks != null); - assert (chunkStartTimes != null); - assert (chunkIndex < chunkStartTimes.length); - return url.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) - .replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes[chunkIndex])); + public Uri buildRequestUri(int track, int chunkIndex) { + Assertions.checkState(tracks != null); + Assertions.checkState(chunkStartTimes != null); + Assertions.checkState(chunkIndex < chunkStartTimes.size()); + String chunkUrl = chunkTemplate + .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) + .replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes.get(chunkIndex))); + return baseUri.buildUpon().appendEncodedPath(chunkUrl).build(); } } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java deleted file mode 100644 index 8fb6e66e40..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.smoothstreaming; - -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.util.ManifestFetcher; - -import android.net.Uri; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A concrete implementation of {@link ManifestFetcher} for loading SmoothStreaming - * manifests. - *

- * This class is provided for convenience, however it is expected that most applications will - * contain their own mechanisms for making asynchronous network requests and parsing the response. - * In such cases it is recommended that application developers use their existing solution rather - * than this one. - */ -public final class SmoothStreamingManifestFetcher extends ManifestFetcher { - - private final SmoothStreamingManifestParser parser; - - /** - * @param callback The callback to provide with the parsed manifest (or error). - */ - public SmoothStreamingManifestFetcher(ManifestCallback callback) { - super(callback); - parser = new SmoothStreamingManifestParser(); - } - - /** - * @param callback The callback to provide with the parsed manifest (or error). - * @param timeoutMillis The timeout in milliseconds for the connection used to load the data. - */ - public SmoothStreamingManifestFetcher(ManifestCallback callback, - int timeoutMillis) { - super(callback, timeoutMillis); - parser = new SmoothStreamingManifestParser(); - } - - @Override - protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding, - String contentId, Uri baseUrl) throws IOException, ParserException { - return parser.parse(stream, inputEncoding); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index b928cc5c16..46d0d41fed 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -21,7 +21,9 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.ManifestParser; +import android.net.Uri; import android.util.Base64; import android.util.Pair; @@ -31,6 +33,7 @@ import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -41,7 +44,7 @@ import java.util.UUID; * @see * IIS Smooth Streaming Client Manifest Format */ -public class SmoothStreamingManifestParser { +public class SmoothStreamingManifestParser implements ManifestParser { private final XmlPullParserFactory xmlParserFactory; @@ -53,21 +56,13 @@ public class SmoothStreamingManifestParser { } } - /** - * Parses a manifest from the provided {@link InputStream}. - * - * @param inputStream The stream from which to parse the manifest. - * @param inputEncoding The encoding of the input. - * @return The parsed manifest. - * @throws IOException If a problem occurred reading from the stream. - * @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest. - */ - public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws - IOException, ParserException { + @Override + public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding, + String contentId, Uri baseUri) throws IOException, ParserException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); xmlParser.setInput(inputStream, inputEncoding); - SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null); + SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, baseUri); return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); } catch (XmlPullParserException e) { throw new ParserException(e); @@ -90,14 +85,16 @@ public class SmoothStreamingManifestParser { */ private static abstract class ElementParser { + private final Uri baseUri; private final String tag; private final ElementParser parent; private final List> normalizedAttributes; - public ElementParser(String tag, ElementParser parent) { - this.tag = tag; + public ElementParser(ElementParser parent, Uri baseUri, String tag) { this.parent = parent; + this.baseUri = baseUri; + this.tag = tag; this.normalizedAttributes = new LinkedList>(); } @@ -120,7 +117,7 @@ public class SmoothStreamingManifestParser { } else if (handleChildInline(tagName)) { parseStartTag(xmlParser); } else { - ElementParser childElementParser = newChildParser(this, tagName); + ElementParser childElementParser = newChildParser(this, tagName, baseUri); if (childElementParser == null) { skippingElementDepth = 1; } else { @@ -157,13 +154,13 @@ public class SmoothStreamingManifestParser { } } - private ElementParser newChildParser(ElementParser parent, String name) { + private ElementParser newChildParser(ElementParser parent, String name, Uri baseUri) { if (TrackElementParser.TAG.equals(name)) { - return new TrackElementParser(parent); + return new TrackElementParser(parent, baseUri); } else if (ProtectionElementParser.TAG.equals(name)) { - return new ProtectionElementParser(parent); + return new ProtectionElementParser(parent, baseUri); } else if (StreamElementParser.TAG.equals(name)) { - return new StreamElementParser(parent); + return new StreamElementParser(parent, baseUri); } return null; } @@ -308,6 +305,15 @@ public class SmoothStreamingManifestParser { } } + protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) { + String value = parser.getAttributeValue(null, key); + if (value != null) { + return Boolean.parseBoolean(value); + } else { + return defaultValue; + } + } + } private static class SmoothStreamMediaParser extends ElementParser { @@ -317,19 +323,23 @@ public class SmoothStreamingManifestParser { private static final String KEY_MAJOR_VERSION = "MajorVersion"; private static final String KEY_MINOR_VERSION = "MinorVersion"; private static final String KEY_TIME_SCALE = "TimeScale"; + private static final String KEY_DVR_WINDOW_LENGTH = "DVRWindowLength"; private static final String KEY_DURATION = "Duration"; private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount"; + private static final String KEY_IS_LIVE = "IsLive"; private int majorVersion; private int minorVersion; - private long timeScale; + private long timescale; private long duration; + private long dvrWindowLength; private int lookAheadCount; + private boolean isLive; private ProtectionElement protectionElement; private List streamElements; - public SmoothStreamMediaParser(ElementParser parent) { - super(TAG, parent); + public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) { + super(parent, baseUri, TAG); lookAheadCount = -1; protectionElement = null; streamElements = new LinkedList(); @@ -339,10 +349,12 @@ public class SmoothStreamingManifestParser { public void parseStartTag(XmlPullParser parser) throws ParserException { majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION); minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION); - timeScale = parseLong(parser, KEY_TIME_SCALE, 10000000L); + timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L); duration = parseRequiredLong(parser, KEY_DURATION); + dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0); lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1); - putNormalizedAttribute(KEY_TIME_SCALE, timeScale); + isLive = parseBoolean(parser, KEY_IS_LIVE, false); + putNormalizedAttribute(KEY_TIME_SCALE, timescale); } @Override @@ -359,8 +371,8 @@ public class SmoothStreamingManifestParser { public Object build() { StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; streamElements.toArray(streamElementArray); - return new SmoothStreamingManifest(majorVersion, minorVersion, timeScale, duration, - lookAheadCount, protectionElement, streamElementArray); + return new SmoothStreamingManifest(majorVersion, minorVersion, timescale, duration, + dvrWindowLength, lookAheadCount, isLive, protectionElement, streamElementArray); } } @@ -376,8 +388,8 @@ public class SmoothStreamingManifestParser { private UUID uuid; private byte[] initData; - public ProtectionElementParser(ElementParser parent) { - super(TAG, parent); + public ProtectionElementParser(ElementParser parent, Uri baseUri) { + super(parent, baseUri, TAG); } @Override @@ -426,7 +438,6 @@ public class SmoothStreamingManifestParser { private static final String KEY_TYPE_TEXT = "text"; private static final String KEY_SUB_TYPE = "Subtype"; private static final String KEY_NAME = "Name"; - private static final String KEY_CHUNKS = "Chunks"; private static final String KEY_QUALITY_LEVELS = "QualityLevels"; private static final String KEY_URL = "Url"; private static final String KEY_MAX_WIDTH = "MaxWidth"; @@ -439,11 +450,12 @@ public class SmoothStreamingManifestParser { private static final String KEY_FRAGMENT_DURATION = "d"; private static final String KEY_FRAGMENT_START_TIME = "t"; + private final Uri baseUri; private final List tracks; private int type; private String subType; - private long timeScale; + private long timescale; private String name; private int qualityLevels; private String url; @@ -452,13 +464,13 @@ public class SmoothStreamingManifestParser { private int displayWidth; private int displayHeight; private String language; - private long[] startTimes; + private ArrayList startTimes; - private int chunkIndex; private long previousChunkDuration; - public StreamElementParser(ElementParser parent) { - super(TAG, parent); + public StreamElementParser(ElementParser parent, Uri baseUri) { + super(parent, baseUri, TAG); + this.baseUri = baseUri; tracks = new LinkedList(); } @@ -477,19 +489,21 @@ public class SmoothStreamingManifestParser { } private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException { - startTimes[chunkIndex] = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L); - if (startTimes[chunkIndex] == -1L) { + int chunkIndex = startTimes.size(); + long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L); + if (startTime == -1L) { if (chunkIndex == 0) { // Assume the track starts at t = 0. - startTimes[chunkIndex] = 0; + startTime = 0; } else if (previousChunkDuration != -1L) { // Infer the start time from the previous chunk's start time and duration. - startTimes[chunkIndex] = startTimes[chunkIndex - 1] + previousChunkDuration; + startTime = startTimes.get(chunkIndex - 1) + previousChunkDuration; } else { // We don't have the start time, and we're unable to infer it. throw new ParserException("Unable to infer start time"); } } + startTimes.add(startTime); previousChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L); chunkIndex++; } @@ -510,11 +524,11 @@ public class SmoothStreamingManifestParser { displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1); displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1); language = parser.getAttributeValue(null, KEY_LANGUAGE); - timeScale = parseInt(parser, KEY_TIME_SCALE, -1); - if (timeScale == -1) { - timeScale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); + timescale = parseInt(parser, KEY_TIME_SCALE, -1); + if (timescale == -1) { + timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); } - startTimes = new long[parseRequiredInt(parser, KEY_CHUNKS)]; + startTimes = new ArrayList(); } private int parseType(XmlPullParser parser) throws ParserException { @@ -544,8 +558,8 @@ public class SmoothStreamingManifestParser { public Object build() { TrackElement[] trackElements = new TrackElement[tracks.size()]; tracks.toArray(trackElements); - return new StreamElement(type, subType, timeScale, name, qualityLevels, url, maxWidth, - maxHeight, displayWidth, displayHeight, language, trackElements, startTimes); + return new StreamElement(baseUri, url, type, subType, timescale, name, qualityLevels, + maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes); } } @@ -586,8 +600,8 @@ public class SmoothStreamingManifestParser { private int nalUnitLengthField; private String content; - public TrackElementParser(ElementParser parent) { - super(TAG, parent); + public TrackElementParser(ElementParser parent, Uri baseUri) { + super(parent, baseUri, TAG); this.csd = new LinkedList(); } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java new file mode 100644 index 0000000000..7c5f8574f3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java @@ -0,0 +1,86 @@ +/* + * 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.smoothstreaming; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; + +import android.util.Base64; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SmoothStreamingUtil { + + private SmoothStreamingUtil() {} + + /** + * Builds a {@link MediaFormat} for the specified track of the specified {@link StreamElement}. + * + * @param element The stream element. + * @param track The index of the track for which to build the format. + * @return The format. + */ + public static MediaFormat getMediaFormat(StreamElement element, int track) { + TrackElement trackElement = element.tracks[track]; + String mimeType = trackElement.mimeType; + if (element.type == StreamElement.TYPE_VIDEO) { + MediaFormat format = MediaFormat.createVideoFormat(mimeType, -1, trackElement.maxWidth, + trackElement.maxHeight, Arrays.asList(trackElement.csd)); + format.setMaxVideoDimensions(element.maxWidth, element.maxHeight); + return format; + } else if (element.type == StreamElement.TYPE_AUDIO) { + List csd; + if (trackElement.csd != null) { + csd = Arrays.asList(trackElement.csd); + } else { + csd = Collections.singletonList(CodecSpecificDataUtil.buildAudioSpecificConfig( + trackElement.sampleRate, trackElement.numChannels)); + } + MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels, + trackElement.sampleRate, csd); + return format; + } + // TODO: Do subtitles need a format? MediaFormat supports KEY_LANGUAGE. + return null; + } + + public static byte[] getKeyId(byte[] initData) { + StringBuilder initDataStringBuilder = new StringBuilder(); + for (int i = 0; i < initData.length; i += 2) { + initDataStringBuilder.append((char) initData[i]); + } + String initDataString = initDataStringBuilder.toString(); + String keyIdString = initDataString.substring( + initDataString.indexOf("") + 5, initDataString.indexOf("")); + byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT); + swap(keyId, 0, 3); + swap(keyId, 1, 2); + swap(keyId, 4, 5); + swap(keyId, 6, 7); + return keyId; + } + + private static void swap(byte[] data, int firstPosition, int secondPosition) { + byte temp = data[firstPosition]; + data[firstPosition] = data[secondPosition]; + data[secondPosition] = temp; + } + +} 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 d6504461b2..db5440ead4 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.text; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; @@ -177,7 +178,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { resetSampleHolder = true; InputStream subtitleInputStream = new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size); - subtitle = subtitleParser.parse(subtitleInputStream, "UTF-8", sampleHolder.timeUs); + subtitle = subtitleParser.parse(subtitleInputStream, C.UTF8_NAME, sampleHolder.timeUs); syncNextEventIndex(timeUs); textRendererNeedsUpdate = true; } else if (result == SampleSource.END_OF_STREAM) { diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 5e362f4671..a8b10f2ba0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer.util; -import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.C; import android.net.Uri; import android.os.AsyncTask; @@ -30,7 +30,7 @@ import java.net.URL; * * @param The type of the manifest being parsed. */ -public abstract class ManifestFetcher extends AsyncTask { +public class ManifestFetcher extends AsyncTask { /** * Invoked with the result of a manifest fetch. @@ -59,6 +59,7 @@ public abstract class ManifestFetcher extends AsyncTask { public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000; + private final ManifestParser parser; private final ManifestCallback callback; private final int timeoutMillis; @@ -68,15 +69,18 @@ public abstract class ManifestFetcher extends AsyncTask { /** * @param callback The callback to provide with the parsed manifest (or error). */ - public ManifestFetcher(ManifestCallback callback) { - this(callback, DEFAULT_HTTP_TIMEOUT_MILLIS); + public ManifestFetcher(ManifestParser parser, ManifestCallback callback) { + this(parser, callback, DEFAULT_HTTP_TIMEOUT_MILLIS); } /** + * @param parser Parses the manifest from the loaded data. * @param callback The callback to provide with the parsed manifest (or error). * @param timeoutMillis The timeout in milliseconds for the connection used to load the data. */ - public ManifestFetcher(ManifestCallback callback, int timeoutMillis) { + public ManifestFetcher(ManifestParser parser, ManifestCallback callback, + int timeoutMillis) { + this.parser = parser; this.callback = callback; this.timeoutMillis = timeoutMillis; } @@ -89,11 +93,14 @@ public abstract class ManifestFetcher extends AsyncTask { String inputEncoding = null; InputStream inputStream = null; try { - Uri baseUrl = Util.parseBaseUri(urlString); + Uri baseUri = Util.parseBaseUri(urlString); HttpURLConnection connection = configureHttpConnection(new URL(urlString)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - return parse(inputStream, inputEncoding, contentId, baseUrl); + if (inputEncoding == null) { + inputEncoding = C.UTF8_NAME; + } + return parser.parse(inputStream, inputEncoding, contentId, baseUri); } finally { if (inputStream != null) { inputStream.close(); @@ -114,21 +121,6 @@ public abstract class ManifestFetcher extends AsyncTask { } } - /** - * Reads the {@link InputStream} and parses it into a manifest. Invoked from the - * {@link AsyncTask}'s background thread. - * - * @param stream The input stream to read. - * @param inputEncoding The encoding of the input stream. - * @param contentId The content id of the media. - * @param baseUrl Required where the manifest contains urls that are relative to a base url. May - * be null where this is not the case. - * @throws IOException If an error occurred loading the data. - * @throws ParserException If an error occurred parsing the loaded data. - */ - protected abstract T parse(InputStream stream, String inputEncoding, String contentId, - Uri baseUrl) throws IOException, ParserException; - private HttpURLConnection configureHttpConnection(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(timeoutMillis); diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java new file mode 100644 index 0000000000..d3f1fab1e7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java @@ -0,0 +1,47 @@ +/* + * 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.util; + +import com.google.android.exoplayer.ParserException; + +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Parses a manifest from an {@link InputStream}. + * + * @param The type of the manifest being parsed. + */ +public interface ManifestParser { + + /** + * Parses a manifest from an {@link InputStream}. + * + * @param inputStream The input stream to consume. + * @param inputEncoding The encoding of the input stream. + * @param contentId The content id to which the manifest corresponds. May be null. + * @param baseUri If the manifest contains relative uris, this is the uri they are relative to. + * May be null. + * @return The parsed manifest. + * @throws IOException If an error occurs reading the data. + * @throws ParserException If an error occurs parsing the data. + */ + T parse(InputStream inputStream, String inputEncoding, String contentId, Uri baseUri) + throws IOException, ParserException; + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 5ebd400133..b4277f471a 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -18,17 +18,25 @@ package com.google.android.exoplayer.util; import com.google.android.exoplayer.upstream.DataSource; import android.net.Uri; +import android.text.TextUtils; import java.io.IOException; +import java.math.BigDecimal; import java.net.URL; +import java.text.ParseException; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; +import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Miscellaneous utility functions. @@ -41,6 +49,14 @@ public final class Util { */ public static final int SDK_INT = android.os.Build.VERSION.SDK_INT; + private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( + "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" + + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); + + private static final Pattern XS_DURATION_PATTERN = + Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$"); + private Util() {} /** @@ -128,6 +144,32 @@ public final class Util { return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/'))); } + /** + * Merges a uri and a string to produce a new uri. + *

+ * The uri is built according to the following rules: + *

    + *
  • If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is + * ignored and the uri consists solely of {@code stringUri}. + *
  • If {@code stringUri} is null, then the uri consists solely of {@code baseUrl}. + *
  • Otherwise, the uri consists of the concatenation of {@code baseUri} and {@code stringUri}. + *
+ * + * @param baseUri A uri that can form the base of the merged uri. + * @param stringUri A relative or absolute uri in string form. + * @return The merged uri. + */ + public static Uri getMergedUri(Uri baseUri, String stringUri) { + if (stringUri == null) { + return baseUri; + } + Uri uri = Uri.parse(stringUri); + if (!uri.isAbsolute() && baseUri != null) { + uri = Uri.withAppendedPath(baseUri, stringUri); + } + return uri; + } + /** * Returns the index of the largest value in an array that is less than (or optionally equal to) * a specified key. @@ -212,4 +254,76 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. + * + * @param value The attribute value to parse. + * @return The parsed duration in milliseconds. + */ + public static long parseXsDuration(String value) { + Matcher matcher = XS_DURATION_PATTERN.matcher(value); + if (matcher.matches()) { + String hours = matcher.group(2); + double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0; + String minutes = matcher.group(4); + durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0; + String seconds = matcher.group(6); + durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0; + return (long) (durationSeconds * 1000); + } else { + return (long) (Double.parseDouble(value) * 3600 * 1000); + } + } + + /** + * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since + * the epoch. + * + * @param value The attribute value to parse. + * @return The parsed timestamp in milliseconds since the epoch. + */ + public static long parseXsDateTime(String value) throws ParseException { + Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value); + if (!matcher.matches()) { + throw new ParseException("Invalid date/time format: " + value, 0); + } + + int timezoneShift; + if (matcher.group(9) == null) { + // No time zone specified. + timezoneShift = 0; + } else if (matcher.group(9).equalsIgnoreCase("Z")) { + timezoneShift = 0; + } else { + timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60 + + Integer.valueOf(matcher.group(13)))); + if (matcher.group(11).equals("-")) { + timezoneShift *= -1; + } + } + + Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + + dateTime.clear(); + // Note: The month value is 0-based, hence the -1 on group(2) + dateTime.set(Integer.valueOf(matcher.group(1)), + Integer.valueOf(matcher.group(2)) - 1, + Integer.valueOf(matcher.group(3)), + Integer.valueOf(matcher.group(4)), + Integer.valueOf(matcher.group(5)), + Integer.valueOf(matcher.group(6))); + if (!TextUtils.isEmpty(matcher.group(8))) { + final BigDecimal bd = new BigDecimal("0." + matcher.group(8)); + // we care only for milliseconds, so movePointRight(3) + dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue()); + } + + long time = dateTime.getTimeInMillis(); + if (timezoneShift != 0) { + time -= timezoneShift * 60000; + } + + return time; + } + } From bc01a4f48d3afd2f612e4f91694c36f4f980c42d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:22:40 +0100 Subject: [PATCH 003/110] Relax MediaCodecVideoTrackRenderer.canReconfigureCodec(). Allow non-H264 adaptive decoders. --- .../android/exoplayer/MediaCodecVideoTrackRenderer.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 0fd1f4fd47..08661ad557 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -330,11 +330,9 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, MediaFormat oldFormat, MediaFormat newFormat) { - // TODO: Relax this check to also allow non-H264 adaptive decoders. - return newFormat.mimeType.equals(MimeTypes.VIDEO_H264) - && oldFormat.mimeType.equals(MimeTypes.VIDEO_H264) - && codecIsAdaptive - || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height); + return newFormat.mimeType.equals(oldFormat.mimeType) + && (codecIsAdaptive + || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)); } @Override From 6b2b6ca4799ef7c8a38c86d8ece60e95d70cdfb1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:23:13 +0100 Subject: [PATCH 004/110] Prevent device provisioning when another device provisioning request is already under process. --- .../android/exoplayer/drm/StreamingDrmSessionManager.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index b01955ebaa..e914a8777f 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -71,6 +71,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private Handler postRequestHandler; private int openCount; + private boolean provisioningInProgress; private int state; private MediaCrypto mediaCrypto; private Exception lastException; @@ -179,6 +180,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { return; } state = STATE_CLOSED; + provisioningInProgress = false; mediaDrmHandler.removeCallbacksAndMessages(null); postResponseHandler.removeCallbacksAndMessages(null); postRequestHandler.removeCallbacksAndMessages(null); @@ -212,11 +214,16 @@ public class StreamingDrmSessionManager implements DrmSessionManager { } private void postProvisionRequest() { + if (provisioningInProgress) { + return; + } + provisioningInProgress = true; ProvisionRequest request = mediaDrm.getProvisionRequest(); postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); } private void onProvisionResponse(Object response) { + provisioningInProgress = false; if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { // This event is stale. return; From d85f4abb2ba2ff374720445c7999af9a0b473b63 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:23:50 +0100 Subject: [PATCH 005/110] Support negative media timestamps (if people *really* want to send us them!). --- .../android/exoplayer/MediaCodecAudioTrackRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 dbe7ac47cc..a43c405dc7 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -263,7 +263,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onEnabled(long timeUs, boolean joining) { super.onEnabled(timeUs, joining); - lastReportedCurrentPositionUs = 0; + lastReportedCurrentPositionUs = Long.MIN_VALUE; } @Override @@ -573,7 +573,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { super.seekTo(timeUs); // TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed. releaseAudioTrack(); - lastReportedCurrentPositionUs = 0; + lastReportedCurrentPositionUs = Long.MIN_VALUE; } @Override @@ -613,7 +613,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { // time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to // allow time to jump backwards if it really wants to. audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); - lastReportedCurrentPositionUs = 0; + lastReportedCurrentPositionUs = Long.MIN_VALUE; } } From e4b35e884aec5313f4b2a0f2102fe359433f7cc6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:26:43 +0100 Subject: [PATCH 006/110] Transition ExoPlayer to use longs for ms timestamps. --- .../demo/full/FullPlayerActivity.java | 2 +- .../demo/full/player/DemoPlayer.java | 6 +++--- .../demo/simple/SimplePlayerActivity.java | 2 +- .../google/android/exoplayer/ExoPlayer.java | 10 +++++----- .../android/exoplayer/ExoPlayerImpl.java | 14 ++++++------- .../exoplayer/ExoPlayerImplInternal.java | 20 +++++++++---------- .../parser/mp4/FragmentedMp4Extractor.java | 4 ++-- .../exoplayer/parser/mp4/TrackFragment.java | 8 ++++---- .../android/exoplayer/util/PlayerControl.java | 4 ++-- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index da8c1cd249..55ef6f56ee 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -70,7 +70,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private boolean playerNeedsPrepare; private boolean autoPlay = true; - private int playerPosition; + private long playerPosition; private boolean enableBackgroundAudio = false; private Uri contentUri; 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 bdaa2e5a73..211d08dcd7 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 @@ -310,7 +310,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi player.setPlayWhenReady(playWhenReady); } - public void seekTo(int positionMs) { + public void seekTo(long positionMs) { player.seekTo(positionMs); } @@ -339,11 +339,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi return playerState; } - public int getCurrentPosition() { + public long getCurrentPosition() { return player.getCurrentPosition(); } - public int getDuration() { + public long getDuration() { return player.getDuration(); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index fa66b5c552..26f8c739a4 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -76,7 +76,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private MediaCodecVideoTrackRenderer videoRenderer; private boolean autoPlay = true; - private int playerPosition; + private long playerPosition; private Uri contentUri; private int contentType; 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 244a31eaf5..a5bc989a40 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java @@ -229,7 +229,7 @@ public interface ExoPlayer { /** * Represents an unknown time or duration. */ - public static final int UNKNOWN_TIME = -1; + public static final long UNKNOWN_TIME = -1; /** * Gets the {@link Looper} associated with the playback thread. @@ -313,7 +313,7 @@ public interface ExoPlayer { * * @param positionMs The seek position. */ - public void seekTo(int positionMs); + public void seekTo(long positionMs); /** * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention @@ -363,14 +363,14 @@ public interface ExoPlayer { * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the * duration is not known. */ - public int getDuration(); + public long getDuration(); /** * Gets the current playback position in milliseconds. * * @return The current playback position in milliseconds. */ - public int getCurrentPosition(); + public long getCurrentPosition(); /** * Gets an estimate of the absolute position in milliseconds up to which data is buffered. @@ -378,7 +378,7 @@ public interface ExoPlayer { * @return An estimate of the absolute position in milliseconds up to which data is buffered, * or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available. */ - public int getBufferedPosition(); + public long getBufferedPosition(); /** * Gets an estimate of the percentage into the media up to which data is buffered. diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java index efbe5ce1e8..22f11c13b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java @@ -130,7 +130,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override - public void seekTo(int positionMs) { + public void seekTo(long positionMs) { internalPlayer.seekTo(positionMs); } @@ -156,26 +156,26 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override - public int getDuration() { + public long getDuration() { return internalPlayer.getDuration(); } @Override - public int getCurrentPosition() { + public long getCurrentPosition() { return internalPlayer.getCurrentPosition(); } @Override - public int getBufferedPosition() { + public long getBufferedPosition() { return internalPlayer.getBufferedPosition(); } @Override public int getBufferedPercentage() { - int bufferedPosition = getBufferedPosition(); - int duration = getDuration(); + long bufferedPosition = getBufferedPosition(); + long duration = getDuration(); return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0 - : (duration == 0 ? 100 : (bufferedPosition * 100) / duration); + : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); } // Not private so it can be called from an inner class without going through a thunk method. 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 9dc6228fa9..bb01a2bf58 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -117,18 +117,18 @@ import java.util.List; return internalPlaybackThread.getLooper(); } - public int getCurrentPosition() { - return (int) (positionUs / 1000); + public long getCurrentPosition() { + return positionUs / 1000; } - public int getBufferedPosition() { + public long getBufferedPosition() { return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME - : (int) (bufferedPositionUs / 1000); + : bufferedPositionUs / 1000; } - public int getDuration() { + public long getDuration() { return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME - : (int) (durationUs / 1000); + : durationUs / 1000; } public void prepare(TrackRenderer... renderers) { @@ -139,8 +139,8 @@ import java.util.List; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } - public void seekTo(int positionMs) { - handler.obtainMessage(MSG_SEEK_TO, positionMs, 0).sendToTarget(); + public void seekTo(long positionMs) { + handler.obtainMessage(MSG_SEEK_TO, positionMs).sendToTarget(); } public void stop() { @@ -204,7 +204,7 @@ import java.util.List; return true; } case MSG_SEEK_TO: { - seekToInternal(msg.arg1); + seekToInternal((Long) msg.obj); return true; } case MSG_STOP: { @@ -453,7 +453,7 @@ import java.util.List; } } - private void seekToInternal(int positionMs) throws ExoPlaybackException { + private void seekToInternal(long positionMs) throws ExoPlaybackException { rebuffering = false; positionUs = positionMs * 1000L; mediaClock.stop(); 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 3267d5b409..93c5053b6d 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 @@ -855,8 +855,8 @@ public final class FragmentedMp4Extractor implements Extractor { out.initTables(sampleCount); int[] sampleSizeTable = out.sampleSizeTable; - int[] sampleDecodingTimeTable = out.sampleDecodingTimeTable; int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable; + long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable; boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable; long timescale = track.timescale; @@ -882,7 +882,7 @@ public final class FragmentedMp4Extractor implements Extractor { } else { sampleCompositionTimeOffsetTable[i] = 0; } - sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale); + sampleDecodingTimeTable[i] = (cumulativeTime * 1000) / timescale; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); 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 e2e08225b2..a25dfa4505 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 @@ -32,14 +32,14 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; * 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; + /** + * The decoding time of each sample in the run. + */ + public long[] sampleDecodingTimeTable; /** * Indicates which samples are sync frames. */ diff --git a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java index ba454584fe..17027a8d91 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java +++ b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java @@ -70,12 +70,12 @@ public class PlayerControl implements MediaPlayerControl { @Override public int getCurrentPosition() { - return exoPlayer.getCurrentPosition(); + return (int) exoPlayer.getCurrentPosition(); } @Override public int getDuration() { - return exoPlayer.getDuration(); + return (int) exoPlayer.getDuration(); } @Override From 6c3ae7f1757789936294b6ba79586dbf7ca43f31 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:30:39 +0100 Subject: [PATCH 007/110] Add SubtitleView and CaptionStyleCompat to ExoPlayer. --- .../demo/full/FullPlayerActivity.java | 57 +++- .../main/res/layout/player_activity_full.xml | 14 +- demo/src/main/res/values/constants.xml | 22 ++ .../exoplayer/parser/mp4/TrackFragment.java | 4 +- .../exoplayer/text/CaptionStyleCompat.java | 142 +++++++++ .../android/exoplayer/text/SubtitleView.java | 295 ++++++++++++++++++ 6 files changed, 519 insertions(+), 15 deletions(-) create mode 100644 demo/src/main/res/values/constants.xml create mode 100644 library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java create mode 100644 library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 55ef6f56ee..366118877f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -24,13 +24,20 @@ import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.text.CaptionStyleCompat; +import com.google.android.exoplayer.text.SubtitleView; +import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.VerboseLogUtil; +import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; +import android.view.Display; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; @@ -38,6 +45,8 @@ import android.view.SurfaceHolder; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; +import android.view.WindowManager; +import android.view.accessibility.CaptioningManager; import android.widget.Button; import android.widget.MediaController; import android.widget.PopupMenu; @@ -50,6 +59,7 @@ import android.widget.TextView; public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, DemoPlayer.Listener, DemoPlayer.TextListener { + private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int MENU_GROUP_TRACKS = 1; private static final int ID_OFFSET = 2; @@ -60,7 +70,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private VideoSurfaceView surfaceView; private TextView debugTextView; private TextView playerStateTextView; - private TextView subtitlesTextView; + private SubtitleView subtitleView; private Button videoButton; private Button audioButton; private Button textButton; @@ -108,7 +118,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba debugTextView = (TextView) findViewById(R.id.debug_text_view); playerStateTextView = (TextView) findViewById(R.id.player_state_view); - subtitlesTextView = (TextView) findViewById(R.id.subtitles); + subtitleView = (SubtitleView) findViewById(R.id.subtitles); mediaController = new MediaController(this); mediaController.setAnchorView(root); @@ -122,6 +132,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba @Override public void onResume() { super.onResume(); + configureSubtitleView(); preparePlayer(); } @@ -380,10 +391,10 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba @Override public void onText(String text) { if (TextUtils.isEmpty(text)) { - subtitlesTextView.setVisibility(View.INVISIBLE); + subtitleView.setVisibility(View.INVISIBLE); } else { - subtitlesTextView.setVisibility(View.VISIBLE); - subtitlesTextView.setText(text); + subtitleView.setVisibility(View.VISIBLE); + subtitleView.setText(text); } } @@ -409,4 +420,40 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } + private void configureSubtitleView() { + CaptionStyleCompat captionStyle; + float captionTextSize = getCaptionFontSize(); + if (Util.SDK_INT >= 19) { + captionStyle = getUserCaptionStyleV19(); + captionTextSize *= getUserCaptionFontScaleV19(); + } else { + captionStyle = CaptionStyleCompat.DEFAULT; + } + subtitleView.setStyle(captionStyle); + subtitleView.setTextSize(captionTextSize); + } + + private float getCaptionFontSize() { + Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + Point displaySize = new Point(); + display.getSize(displaySize); + return Math.max(getResources().getDimension(R.dimen.subtitle_minimum_font_size), + CAPTION_LINE_HEIGHT_RATIO * Math.min(displaySize.x, displaySize.y)); + } + + @TargetApi(19) + private float getUserCaptionFontScaleV19() { + CaptioningManager captioningManager = + (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); + return captioningManager.getFontScale(); + } + + @TargetApi(19) + private CaptionStyleCompat getUserCaptionStyleV19() { + CaptioningManager captioningManager = + (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); + return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); + } + } diff --git a/demo/src/main/res/layout/player_activity_full.xml b/demo/src/main/res/layout/player_activity_full.xml index 8d3e132995..d2e069620f 100644 --- a/demo/src/main/res/layout/player_activity_full.xml +++ b/demo/src/main/res/layout/player_activity_full.xml @@ -24,15 +24,13 @@ android:layout_height="match_parent" android:layout_gravity="center"/> - + + + + + + 13sp + + 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 a25dfa4505..4291f5cad4 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 @@ -95,8 +95,8 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; // 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]; + sampleDecodingTimeTable = new long[tableSize]; sampleIsSyncFrameTable = new boolean[tableSize]; sampleHasSubsampleEncryptionTable = new boolean[tableSize]; } @@ -147,7 +147,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; return true; } - public int getSamplePresentationTime(int index) { + public long getSamplePresentationTime(int index) { return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java new file mode 100644 index 0000000000..60cc8ab129 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java @@ -0,0 +1,142 @@ +/* + * 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.text; + +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.accessibility.CaptioningManager; +import android.view.accessibility.CaptioningManager.CaptionStyle; + +/** + * A compatibility wrapper for {@link CaptionStyle}. + */ +public final class CaptionStyleCompat { + + /** + * Edge type value specifying no character edges. + */ + public static final int EDGE_TYPE_NONE = 0; + + /** + * Edge type value specifying uniformly outlined character edges. + */ + public static final int EDGE_TYPE_OUTLINE = 1; + + /** + * Edge type value specifying drop-shadowed character edges. + */ + public static final int EDGE_TYPE_DROP_SHADOW = 2; + + /** + * Edge type value specifying raised bevel character edges. + */ + public static final int EDGE_TYPE_RAISED = 3; + + /** + * Edge type value specifying depressed bevel character edges. + */ + public static final int EDGE_TYPE_DEPRESSED = 4; + + /** + * Use color setting specified by the track and fallback to default caption style. + */ + public static final int USE_TRACK_COLOR_SETTINGS = 1; + + /** + * Default caption style. + */ + public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat( + Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null); + + /** + * The preferred foreground color. + */ + public final int foregroundColor; + + /** + * The preferred background color. + */ + public final int backgroundColor; + + /** + * The preferred window color. + */ + public final int windowColor; + + /** + * The preferred edge type. One of: + *
    + *
  • {@link #EDGE_TYPE_NONE} + *
  • {@link #EDGE_TYPE_OUTLINE} + *
  • {@link #EDGE_TYPE_DROP_SHADOW} + *
  • {@link #EDGE_TYPE_RAISED} + *
  • {@link #EDGE_TYPE_DEPRESSED} + *
+ */ + public final int edgeType; + + /** + * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. + */ + public final int edgeColor; + + /** + * The preferred typeface. + */ + public final Typeface typeface; + + /** + * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. + * + * @param style A {@link CaptionStyle}. + * @return The equivalent {@link CaptionStyleCompat}. + */ + @TargetApi(19) + public static CaptionStyleCompat createFromCaptionStyle(CaptionStyle style) { + int windowColor = Util.SDK_INT >= 21 ? getWindowColorV21(style) : Color.TRANSPARENT; + return new CaptionStyleCompat(style.foregroundColor, style.backgroundColor, windowColor, + style.edgeType, style.edgeColor, style.getTypeface()); + } + + /** + * @param foregroundColor See {@link #foregroundColor}. + * @param backgroundColor See {@link #backgroundColor}. + * @param windowColor See {@link #windowColor}. + * @param edgeType See {@link #edgeType}. + * @param edgeColor See {@link #edgeColor}. + * @param typeface See {@link #typeface}. + */ + public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, int edgeType, + int edgeColor, Typeface typeface) { + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + this.windowColor = windowColor; + this.edgeType = edgeType; + this.edgeColor = edgeColor; + this.typeface = typeface; + } + + @SuppressWarnings("unused") + @TargetApi(21) + private static int getWindowColorV21(CaptioningManager.CaptionStyle captionStyle) { + // TODO: Uncomment when building against API level 21. + return Color.TRANSPARENT; //captionStyle.windowColor; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java new file mode 100644 index 0000000000..7b2ecf5494 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java @@ -0,0 +1,295 @@ +/* + * 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.text; + +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Join; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.View; + +/** + * A view for rendering captions. + *

+ * The caption style and text size can be configured using {@link #setStyle(CaptionStyleCompat)} and + * {@link #setTextSize(float)} respectively. + */ +public class SubtitleView extends View { + + /** + * Ratio of inner padding to font size. + */ + private static final float INNER_PADDING_RATIO = 0.125f; + + /** + * Temporary rectangle used for computing line bounds. + */ + private final RectF lineBounds = new RectF(); + + /** + * Reusable string builder used for holding text. + */ + private final StringBuilder textBuilder = new StringBuilder(); + + // Styled dimensions. + private final float cornerRadius; + private final float outlineWidth; + private final float shadowRadius; + private final float shadowOffset; + + private TextPaint textPaint; + private Paint paint; + + private int foregroundColor; + private int backgroundColor; + private int edgeColor; + private int edgeType; + + private boolean hasMeasurements; + private int lastMeasuredWidth; + private StaticLayout layout; + + private float spacingMult; + private float spacingAdd; + private int innerPaddingX; + + public SubtitleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + int[] viewAttr = {android.R.attr.text, android.R.attr.textSize, + android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier}; + TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0); + CharSequence text = a.getText(0); + int textSize = a.getDimensionPixelSize(1, 15); + spacingAdd = a.getDimensionPixelSize(2, 0); + spacingMult = a.getFloat(3, 1); + a.recycle(); + + Resources resources = getContext().getResources(); + DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + int twoDpInPx = Math.round((2 * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); + cornerRadius = twoDpInPx; + outlineWidth = twoDpInPx; + shadowRadius = twoDpInPx; + shadowOffset = twoDpInPx; + + textPaint = new TextPaint(); + textPaint.setAntiAlias(true); + textPaint.setSubpixelText(true); + + paint = new Paint(); + paint.setAntiAlias(true); + + innerPaddingX = 0; + setText(text); + setTextSize(textSize); + setStyle(CaptionStyleCompat.DEFAULT); + } + + public SubtitleView(Context context) { + this(context, null); + } + + @Override + public void setBackgroundColor(int color) { + backgroundColor = color; + invalidate(); + } + + /** + * Sets the text to be displayed by the view. + * + * @param text The text to display. + */ + public void setText(CharSequence text) { + textBuilder.setLength(0); + textBuilder.append(text); + hasMeasurements = false; + requestLayout(); + } + + /** + * Sets the text size in pixels. + * + * @param size The text size in pixels. + */ + public void setTextSize(float size) { + if (textPaint.getTextSize() != size) { + textPaint.setTextSize(size); + innerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f); + hasMeasurements = false; + requestLayout(); + invalidate(); + } + } + + /** + * Configures the view according to the given style. + * + * @param style A style for the view. + */ + public void setStyle(CaptionStyleCompat style) { + foregroundColor = style.foregroundColor; + backgroundColor = style.backgroundColor; + edgeType = style.edgeType; + edgeColor = style.edgeColor; + setTypeface(style.typeface); + super.setBackgroundColor(style.windowColor); + hasMeasurements = false; + requestLayout(); + } + + private void setTypeface(Typeface typeface) { + if (textPaint.getTypeface() != typeface) { + textPaint.setTypeface(typeface); + hasMeasurements = false; + requestLayout(); + invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSpec = MeasureSpec.getSize(widthMeasureSpec); + + if (computeMeasurements(widthSpec)) { + final StaticLayout layout = this.layout; + final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2; + final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom(); + int width = 0; + int lineCount = layout.getLineCount(); + for (int i = 0; i < lineCount; i++) { + width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width); + } + width += paddingX; + setMeasuredDimension(width, height); + } else if (Util.SDK_INT >= 11) { + setTooSmallMeasureDimensionV11(); + } else { + setMeasuredDimension(0, 0); + } + } + + @TargetApi(11) + private void setTooSmallMeasureDimensionV11() { + setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = r - l; + computeMeasurements(width); + } + + private boolean computeMeasurements(int maxWidth) { + if (hasMeasurements && maxWidth == lastMeasuredWidth) { + return true; + } + + // Account for padding. + final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2; + maxWidth -= paddingX; + if (maxWidth <= 0) { + return false; + } + + hasMeasurements = true; + lastMeasuredWidth = maxWidth; + layout = new StaticLayout(textBuilder, textPaint, maxWidth, null, spacingMult, spacingAdd, + true); + return true; + } + + @Override + protected void onDraw(Canvas c) { + final StaticLayout layout = this.layout; + if (layout == null) { + return; + } + + final int saveCount = c.save(); + final int innerPaddingX = this.innerPaddingX; + c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop()); + + final int lineCount = layout.getLineCount(); + final Paint textPaint = this.textPaint; + final Paint paint = this.paint; + final RectF bounds = lineBounds; + + if (Color.alpha(backgroundColor) > 0) { + final float cornerRadius = this.cornerRadius; + float previousBottom = layout.getLineTop(0); + + paint.setColor(backgroundColor); + paint.setStyle(Style.FILL); + + for (int i = 0; i < lineCount; i++) { + bounds.left = layout.getLineLeft(i) - innerPaddingX; + bounds.right = layout.getLineRight(i) + innerPaddingX; + bounds.top = previousBottom; + bounds.bottom = layout.getLineBottom(i); + previousBottom = bounds.bottom; + + c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint); + } + } + + if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { + textPaint.setStrokeJoin(Join.ROUND); + textPaint.setStrokeWidth(outlineWidth); + textPaint.setColor(edgeColor); + textPaint.setStyle(Style.FILL_AND_STROKE); + layout.draw(c); + } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { + textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); + } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED + || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { + boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; + int colorUp = raised ? Color.WHITE : edgeColor; + int colorDown = raised ? edgeColor : Color.WHITE; + float offset = shadowRadius / 2f; + textPaint.setColor(foregroundColor); + textPaint.setStyle(Style.FILL); + textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp); + layout.draw(c); + textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown); + } + + textPaint.setColor(foregroundColor); + textPaint.setStyle(Style.FILL); + layout.draw(c); + textPaint.setShadowLayer(0, 0, 0, 0); + c.restoreToCount(saveCount); + } + +} From ec90eac301ba212b3bd6c3c70e26c66a65138597 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:34:35 +0100 Subject: [PATCH 008/110] Support anamorphic video content. --- .../exoplayer/demo/full/EventLogger.java | 4 +- .../demo/full/FullPlayerActivity.java | 5 +- .../demo/full/player/DemoPlayer.java | 6 +-- .../demo/simple/SimplePlayerActivity.java | 5 +- .../exoplayer/MediaCodecTrackRenderer.java | 15 +++--- .../MediaCodecVideoTrackRenderer.java | 30 ++++++++++-- .../google/android/exoplayer/MediaFormat.java | 48 +++++++++++++++---- .../android/exoplayer/parser/mp4/Atom.java | 1 + .../parser/mp4/FragmentedMp4Extractor.java | 13 ++++- 9 files changed, 95 insertions(+), 32 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 722e21c45e..84ea1b0f3e 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 @@ -73,8 +73,8 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } @Override - public void onVideoSizeChanged(int width, int height) { - Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); + public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { + Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + pixelWidthHeightRatio + "]"); } // DemoPlayer.InfoListener diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 366118877f..48ed2f5ded 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -260,9 +260,10 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } @Override - public void onVideoSizeChanged(int width, int height) { + public void onVideoSizeChanged(int width, int height, float pixelWidthAspectRatio) { shutterView.setVisibility(View.GONE); - surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height); + surfaceView.setVideoWidthHeightRatio( + height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); } // User controls 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 211d08dcd7..914c9f9798 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 @@ -93,7 +93,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public interface Listener { void onStateChanged(boolean playWhenReady, int playbackState); void onError(Exception e); - void onVideoSizeChanged(int width, int height); + void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); } /** @@ -377,9 +377,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onVideoSizeChanged(int width, int height) { + public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { for (Listener listener : listeners) { - listener.onVideoSizeChanged(width, height); + listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index 26f8c739a4..73d2605c94 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -231,8 +231,9 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call // MediaCodecVideoTrackRenderer.Listener @Override - public void onVideoSizeChanged(int width, int height) { - surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height); + public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { + surfaceView.setVideoWidthHeightRatio( + height == 0 ? 1 : (pixelWidthHeightRatio * width) / height); } @Override 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 a23f5edf49..4124a22c27 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -79,18 +79,19 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } /** - * Value of {@link #sourceState} when the source is not ready. + * Value returned by {@link #getSourceState()} when the source is not ready. */ protected static final int SOURCE_STATE_NOT_READY = 0; /** - * Value of {@link #sourceState} when the source is ready and we're able to read from it. + * Value returned by {@link #getSourceState()} when the source is ready and we're able to read + * from it. */ protected static final int SOURCE_STATE_READY = 1; /** - * Value of {@link #sourceState} when the source is ready but we might not be able to read from - * it. We transition to this state when an attempt to read a sample fails despite the source - * reporting that samples are available. This can occur when the next sample to be provided by - * the source is for another renderer. + * Value returned by {@link #getSourceState()} when the source is ready but we might not be able + * to read from it. We transition to this state when an attempt to read a sample fails despite the + * source reporting that samples are available. This can occur when the next sample to be provided + * by the source is for another renderer. */ protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2; @@ -620,7 +621,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @param formatHolder Holds the new format. * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}. */ - private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { + protected void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { MediaFormat oldFormat = format; format = formatHolder.format; drmInitData = formatHolder.drmInitData; 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 08661ad557..22d529514e 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -58,8 +58,11 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * * @param width The video width in pixels. * @param height The video height in pixels. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case + * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * content. */ - void onVideoSizeChanged(int width, int height); + void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); /** * Invoked when a frame is rendered to a surface for the first time following that surface @@ -98,8 +101,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { private int currentWidth; private int currentHeight; + private float currentPixelWidthHeightRatio; private int lastReportedWidth; private int lastReportedHeight; + private float lastReportedPixelWidthHeightRatio; /** * @param source The upstream source from which the renderer obtains samples. @@ -208,8 +213,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { joiningDeadlineUs = -1; currentWidth = -1; currentHeight = -1; + currentPixelWidthHeightRatio = -1; lastReportedWidth = -1; lastReportedHeight = -1; + lastReportedPixelWidthHeightRatio = -1; } @Override @@ -272,8 +279,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { super.onDisabled(); currentWidth = -1; currentHeight = -1; + currentPixelWidthHeightRatio = -1; lastReportedWidth = -1; lastReportedHeight = -1; + lastReportedPixelWidthHeightRatio = -1; } @Override @@ -315,6 +324,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { codec.setVideoScalingMode(videoScalingMode); } + @Override + protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { + super.onInputFormatChanged(holder); + // TODO: Ideally this would be read in onOutputFormatChanged, but there doesn't seem + // to be a way to pass a custom key/value pair value through to the output format. + currentPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio; + } + @Override protected void onOutputFormatChanged(android.media.MediaFormat format) { boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) @@ -394,10 +411,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { - if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight) { + if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight + || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { lastReportedWidth = currentWidth; lastReportedHeight = currentHeight; - notifyVideoSizeChanged(currentWidth, currentHeight); + lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; + notifyVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); } TraceUtil.beginSection("renderVideoBuffer"); codec.releaseOutputBuffer(bufferIndex, true); @@ -409,12 +428,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } } - private void notifyVideoSizeChanged(final int width, final int height) { + private void notifyVideoSizeChanged(final int width, final int height, + final float pixelWidthHeightRatio) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onVideoSizeChanged(width, height); + eventListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); } }); } 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 d703a72a84..24af7ba1a0 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -31,6 +31,9 @@ import java.util.List; */ public class MediaFormat { + private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO = + "com.google.android.videos.pixelWidthHeightRatio"; + public static final int NO_VALUE = -1; public final String mimeType; @@ -38,6 +41,7 @@ public class MediaFormat { public final int width; public final int height; + public final float pixelWidthHeightRatio; public final int channelCount; public final int sampleRate; @@ -59,14 +63,19 @@ public class MediaFormat { public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, int height, List initializationData) { - return new MediaFormat(mimeType, maxInputSize, width, height, NO_VALUE, NO_VALUE, - initializationData); + return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData); + } + + public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, + int height, float pixelWidthHeightRatio, List initializationData) { + return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE, + NO_VALUE, initializationData); } public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, int sampleRate, List initializationData) { - return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, channelCount, sampleRate, - initializationData); + return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, + sampleRate, initializationData); } @TargetApi(16) @@ -78,6 +87,7 @@ public class MediaFormat { height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); + pixelWidthHeightRatio = getOptionalFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); initializationData = new ArrayList(); for (int i = 0; format.containsKey("csd-" + i); i++) { ByteBuffer buffer = format.getByteBuffer("csd-" + i); @@ -90,12 +100,14 @@ public class MediaFormat { maxHeight = NO_VALUE; } - private MediaFormat(String mimeType, int maxInputSize, int width, int height, int channelCount, - int sampleRate, List initializationData) { + private MediaFormat(String mimeType, int maxInputSize, int width, int height, + float pixelWidthHeightRatio, int channelCount, int sampleRate, + List initializationData) { this.mimeType = mimeType; this.maxInputSize = maxInputSize; this.width = width; this.height = height; + this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.channelCount = channelCount; this.sampleRate = sampleRate; this.initializationData = initializationData == null ? Collections.emptyList() @@ -128,6 +140,7 @@ public class MediaFormat { result = 31 * result + maxInputSize; result = 31 * result + width; result = 31 * result + height; + result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio); result = 31 * result + maxWidth; result = 31 * result + maxHeight; result = 31 * result + channelCount; @@ -163,6 +176,7 @@ public class MediaFormat { private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) { if (maxInputSize != other.maxInputSize || width != other.width || height != other.height + || pixelWidthHeightRatio != other.pixelWidthHeightRatio || (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight)) || channelCount != other.channelCount || sampleRate != other.sampleRate || !Util.areEqual(mimeType, other.mimeType) @@ -179,8 +193,9 @@ public class MediaFormat { @Override public String toString() { - return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " + - channelCount + ", " + sampleRate + ", " + maxWidth + ", " + maxHeight + ")"; + return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " + + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + maxWidth + ", " + + maxHeight + ")"; } /** @@ -196,6 +211,7 @@ public class MediaFormat { maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate); + maybeSetFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio); for (int i = 0; i < initializationData.size(); i++) { format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); } @@ -221,9 +237,21 @@ public class MediaFormat { } @TargetApi(16) - private static final int getOptionalIntegerV16(android.media.MediaFormat format, - String key) { + private static final void maybeSetFloatV16(android.media.MediaFormat format, String key, + float value) { + if (value != NO_VALUE) { + format.setFloat(key, value); + } + } + + @TargetApi(16) + private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) { return format.containsKey(key) ? format.getInteger(key) : NO_VALUE; } + @TargetApi(16) + private static final float getOptionalFloatV16(android.media.MediaFormat format, String key) { + return format.containsKey(key) ? format.getFloat(key) : NO_VALUE; + } + } 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 fbdccc2d67..32eb1937ae 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 @@ -53,6 +53,7 @@ import java.util.ArrayList; public static final int TYPE_saiz = 0x7361697A; public static final int TYPE_uuid = 0x75756964; public static final int TYPE_senc = 0x73656E63; + public static final int TYPE_pasp = 0x70617370; 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 93c5053b6d..8ede9e93b2 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 @@ -106,6 +106,7 @@ public final class FragmentedMp4Extractor implements Extractor { parsedAtoms.add(Atom.TYPE_saiz); parsedAtoms.add(Atom.TYPE_uuid); parsedAtoms.add(Atom.TYPE_senc); + parsedAtoms.add(Atom.TYPE_pasp); PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms); } @@ -529,6 +530,7 @@ public final class FragmentedMp4Extractor implements Extractor { parent.skip(24); int width = parent.readUnsignedShort(); int height = parent.readUnsignedShort(); + float pixelWidthHeightRatio = 1; parent.skip(50); List initializationData = null; @@ -543,12 +545,14 @@ public final class FragmentedMp4Extractor implements Extractor { initializationData = parseAvcCFromParent(parent, childStartPosition); } else if (childAtomType == Atom.TYPE_sinf) { trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); + } else if (childAtomType == Atom.TYPE_pasp) { + pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); } childPosition += childAtomSize; } MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - width, height, initializationData); + width, height, pixelWidthHeightRatio, initializationData); return Pair.create(format, trackEncryptionBox); } @@ -643,6 +647,13 @@ public final class FragmentedMp4Extractor implements Extractor { return trackEncryptionBox; } + private static float parsePaspFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + ATOM_HEADER_SIZE); + int hSpacing = parent.readUnsignedIntToInt(); + int vSpacing = parent.readUnsignedIntToInt(); + return (float) hSpacing / vSpacing; + } + private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + ATOM_HEADER_SIZE; From c19faa63cd97ba8f8ce4cb26b05488cafa81c479 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 18:03:11 +0100 Subject: [PATCH 009/110] A few tiny bug fixes. --- .../google/android/exoplayer/MediaCodecVideoTrackRenderer.java | 3 ++- .../com/google/android/exoplayer/util/ManifestFetcher.java | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) 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 22d529514e..4a6dcc8206 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -329,7 +329,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { super.onInputFormatChanged(holder); // TODO: Ideally this would be read in onOutputFormatChanged, but there doesn't seem // to be a way to pass a custom key/value pair value through to the output format. - currentPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio; + currentPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1 + : holder.format.pixelWidthHeightRatio; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index a8b10f2ba0..1616429ca1 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -97,9 +97,6 @@ public class ManifestFetcher extends AsyncTask { HttpURLConnection connection = configureHttpConnection(new URL(urlString)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - if (inputEncoding == null) { - inputEncoding = C.UTF8_NAME; - } return parser.parse(inputStream, inputEncoding, contentId, baseUri); } finally { if (inputStream != null) { From b2fc944af14c6d87e6eeb48ef5dc8e4bf6906ee7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:26:04 +0100 Subject: [PATCH 010/110] Remove getLoadedData API from ExoPlayer components. This API wasn't particularly nice. Best to remove it whilst hopefully no-one is using it. Leaving the ReadHead abstraction in place, since it might well prove useful in the future. --- .../google/android/exoplayer/chunk/Chunk.java | 12 ----------- .../chunk/SingleSampleMediaChunk.java | 6 ++---- .../exoplayer/upstream/DataSourceStream.java | 20 ------------------- 3 files changed, 2 insertions(+), 36 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 6724e283fb..8a9471c113 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 @@ -134,18 +134,6 @@ public abstract class Chunk implements Loadable { consumeStream(dataSourceStream); } - /** - * Returns a byte array containing the loaded data. If the chunk is partially loaded, this - * method returns the data that has been loaded so far. If nothing has been loaded, null is - * returned. - * - * @return The loaded data or null. - */ - public final byte[] getLoadedData() { - Assertions.checkState(dataSourceStream != null); - return dataSourceStream.getLoadedData(); - } - /** * Invoked by {@link #consume()}. Implementations may override this method if they wish to * consume the loaded data at this point. 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 dfe0d71584..e0b9f91ad0 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 @@ -64,10 +64,8 @@ public class SingleSampleMediaChunk extends MediaChunk { * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param sampleFormat The format of the media contained by the chunk. * @param headerData Custom header data for the sample. May be null. If set, the header data is - * prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is - * however not considered part of the loaded data, and so is not prepended to the data - * returned by {@link #getLoadedData()}. It is also not reflected in the values returned by - * {@link #bytesLoaded()} and {@link #getLength()}. + * prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is not + * reflected in the values returned by {@link #bytesLoaded()} and {@link #getLength()}. */ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat, 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 dc5227e426..5c4dcd65b2 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 @@ -119,26 +119,6 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream return resolvedLength != C.LENGTH_UNBOUNDED && loadPosition == resolvedLength; } - /** - * Returns a byte array containing the loaded data. If the data is partially loaded, this method - * returns the portion of the data that has been loaded so far. If nothing has been loaded, null - * is returned. This method does not use or update the current read position. - *

- * 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. - */ - public final byte[] getLoadedData() { - if (loadPosition == 0) { - return null; - } - - byte[] rawData = new byte[(int) loadPosition]; - read(null, rawData, 0, new ReadHead(), rawData.length); - return rawData; - } - // {@link NonBlockingInputStream} implementation. @Override From 8378019839ae4c37264d0bcd9f37f481c28c7a35 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:29:34 +0100 Subject: [PATCH 011/110] Fix SmoothStreaming where audio FourCC is missing. --- .../SmoothStreamingManifest.java | 25 +----- .../SmoothStreamingManifestParser.java | 50 +++++++---- .../smoothstreaming/SmoothStreamingUtil.java | 86 ------------------- 3 files changed, 38 insertions(+), 123 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 28f046816e..8d5282d3d1 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer.smoothstreaming; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -103,12 +102,9 @@ public class SmoothStreamingManifest { public final int bitrate; // Audio-video - public final String fourCC; public final byte[][] csd; public final int profile; public final int level; - - // Audio-video (derived) public final String mimeType; // Video-only @@ -125,12 +121,12 @@ public class SmoothStreamingManifest { public final int nalUnitLengthField; public final String content; - public TrackElement(int index, int bitrate, String fourCC, byte[][] csd, int profile, int level, - int maxWidth, int maxHeight, int sampleRate, int channels, int packetSize, int audioTag, - int bitPerSample, int nalUnitLengthField, String content) { + public TrackElement(int index, int bitrate, String mimeType, byte[][] csd, int profile, + int level, int maxWidth, int maxHeight, int sampleRate, int channels, int packetSize, + int audioTag, int bitPerSample, int nalUnitLengthField, String content) { this.index = index; this.bitrate = bitrate; - this.fourCC = fourCC; + this.mimeType = mimeType; this.csd = csd; this.profile = profile; this.level = level; @@ -143,19 +139,6 @@ public class SmoothStreamingManifest { this.bitPerSample = bitPerSample; this.nalUnitLengthField = nalUnitLengthField; this.content = content; - this.mimeType = fourCCToMimeType(fourCC); - } - - private static String fourCCToMimeType(String fourCC) { - if (fourCC.equalsIgnoreCase("H264") || fourCC.equalsIgnoreCase("AVC1") - || fourCC.equalsIgnoreCase("DAVC")) { - return MimeTypes.VIDEO_H264; - } else if (fourCC.equalsIgnoreCase("AACL") || fourCC.equalsIgnoreCase("AACH")) { - return MimeTypes.AUDIO_AAC; - } else if (fourCC.equalsIgnoreCase("TTML")) { - return MimeTypes.APPLICATION_TTML; - } - return null; } } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index 46d0d41fed..ae4cd13b36 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Trac import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.ManifestParser; +import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; import android.util.Base64; @@ -586,7 +587,7 @@ public class SmoothStreamingManifestParser implements ManifestParser csd; - if (trackElement.csd != null) { - csd = Arrays.asList(trackElement.csd); - } else { - csd = Collections.singletonList(CodecSpecificDataUtil.buildAudioSpecificConfig( - trackElement.sampleRate, trackElement.numChannels)); - } - MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels, - trackElement.sampleRate, csd); - return format; - } - // TODO: Do subtitles need a format? MediaFormat supports KEY_LANGUAGE. - return null; - } - - public static byte[] getKeyId(byte[] initData) { - StringBuilder initDataStringBuilder = new StringBuilder(); - for (int i = 0; i < initData.length; i += 2) { - initDataStringBuilder.append((char) initData[i]); - } - String initDataString = initDataStringBuilder.toString(); - String keyIdString = initDataString.substring( - initDataString.indexOf("") + 5, initDataString.indexOf("")); - byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT); - swap(keyId, 0, 3); - swap(keyId, 1, 2); - swap(keyId, 4, 5); - swap(keyId, 6, 7); - return keyId; - } - - private static void swap(byte[] data, int firstPosition, int secondPosition) { - byte temp = data[firstPosition]; - data[firstPosition] = data[secondPosition]; - data[secondPosition] = temp; - } - -} From bf95592b2c7f1998b676b0fe5c21af3ee9fbe907 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:30:21 +0100 Subject: [PATCH 012/110] Remove unused import. Tweak documentation. --- .../com/google/android/exoplayer/util/ManifestFetcher.java | 2 -- .../java/com/google/android/exoplayer/util/ManifestParser.java | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 1616429ca1..804b1240e7 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer.util; -import com.google.android.exoplayer.C; - import android.net.Uri; import android.os.AsyncTask; diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java index d3f1fab1e7..ba997a9f77 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java @@ -33,7 +33,8 @@ public interface ManifestParser { * Parses a manifest from an {@link InputStream}. * * @param inputStream The input stream to consume. - * @param inputEncoding The encoding of the input stream. + * @param inputEncoding The encoding of the input stream. May be null if the input encoding is + * unknown. * @param contentId The content id to which the manifest corresponds. May be null. * @param baseUri If the manifest contains relative uris, this is the uri they are relative to. * May be null. From 4e96caa623d1ceed484d06e049bc8c27c1e9aea7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:31:17 +0100 Subject: [PATCH 013/110] Resolve reference Uris correctly. Ignore the path of the base Uri if the reference starts with "/". Spec - http://tools.ietf.org/html/rfc3986#section-5.2.2 --- .../google/android/exoplayer/util/Util.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index b4277f471a..e7984ef72b 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -163,11 +163,21 @@ public final class Util { if (stringUri == null) { return baseUri; } - Uri uri = Uri.parse(stringUri); - if (!uri.isAbsolute() && baseUri != null) { - uri = Uri.withAppendedPath(baseUri, stringUri); + if (baseUri == null) { + return Uri.parse(stringUri); } - return uri; + if (stringUri.startsWith("/")) { + return new Uri.Builder() + .scheme(baseUri.getScheme()) + .authority(baseUri.getAuthority()) + .appendEncodedPath(stringUri) + .build(); + } + Uri uri = Uri.parse(stringUri); + if (uri.isAbsolute()) { + return uri; + } + return Uri.withAppendedPath(baseUri, stringUri); } /** From f52742b10030cbd55d0a3ed3c2f4ccddbdbed740 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:32:21 +0100 Subject: [PATCH 014/110] Ensure that we try and call release on a renderer Do this even if stop/disable fails. --- .../exoplayer/ExoPlayerImplInternal.java | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) 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 bb01a2bf58..1aaac4ebcf 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -491,21 +491,9 @@ import java.util.List; return; } for (int i = 0; i < renderers.length; i++) { - try { - TrackRenderer renderer = renderers[i]; - ensureStopped(renderer); - if (renderer.getState() == TrackRenderer.STATE_ENABLED) { - renderer.disable(); - } - renderer.release(); - } catch (ExoPlaybackException e) { - // There's nothing we can do. Catch the exception here so that other renderers still have - // a chance of being cleaned up correctly. - Log.e(TAG, "Stop failed.", e); - } catch (RuntimeException e) { - // Ditto. - Log.e(TAG, "Stop failed.", e); - } + TrackRenderer renderer = renderers[i]; + stopAndDisable(renderer); + release(renderer); } renderers = null; timeSourceTrackRenderer = null; @@ -513,6 +501,33 @@ import java.util.List; setState(ExoPlayer.STATE_IDLE); } + private void stopAndDisable(TrackRenderer renderer) { + try { + ensureStopped(renderer); + if (renderer.getState() == TrackRenderer.STATE_ENABLED) { + renderer.disable(); + } + } catch (ExoPlaybackException e) { + // There's nothing we can do. + Log.e(TAG, "Stop failed.", e); + } catch (RuntimeException e) { + // Ditto. + Log.e(TAG, "Stop failed.", e); + } + } + + private void release(TrackRenderer renderer) { + try { + renderer.release(); + } catch (ExoPlaybackException e) { + // There's nothing we can do. + Log.e(TAG, "Release failed.", e); + } catch (RuntimeException e) { + // Ditto. + Log.e(TAG, "Release failed.", e); + } + } + private void sendMessageInternal(int what, Object obj) throws ExoPlaybackException { try { From 525b309764303af559f4c37add838983128a2d6e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:33:23 +0100 Subject: [PATCH 015/110] SmoothStreaming - Parse last chunk duration. --- .../smoothstreaming/SmoothStreamingManifest.java | 16 +++++++++++++++- .../SmoothStreamingManifestParser.java | 11 ++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 8d5282d3d1..08a8f0a7b1 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -173,11 +173,12 @@ public class SmoothStreamingManifest { private final String chunkTemplate; private final List chunkStartTimes; + private final long lastChunkDuration; public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, int displayWidth, int displayHeight, String language, TrackElement[] tracks, - List chunkStartTimes) { + List chunkStartTimes, long lastChunkDuration) { this.baseUri = baseUri; this.chunkTemplate = chunkTemplate; this.type = type; @@ -193,6 +194,7 @@ public class SmoothStreamingManifest { this.tracks = tracks; this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; + this.lastChunkDuration = lastChunkDuration; } /** @@ -215,6 +217,18 @@ public class SmoothStreamingManifest { return (chunkStartTimes.get(chunkIndex) * 1000000L) / timescale; } + /** + * Gets the duration of the specified chunk. + * + * @param chunkIndex The index of the chunk. + * @return The duration of the chunk, in microseconds. + */ + public long getChunkDurationUs(int chunkIndex) { + long chunkDuration = (chunkIndex == chunkCount - 1) ? lastChunkDuration + : chunkStartTimes.get(chunkIndex + 1) - chunkStartTimes.get(chunkIndex); + return chunkDuration / timescale; + } + /** * Builds a uri for requesting the specified chunk of the specified track. * diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index ae4cd13b36..1114c1e4d0 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -467,7 +467,7 @@ public class SmoothStreamingManifestParser implements ManifestParser startTimes; - private long previousChunkDuration; + private long lastChunkDuration; public StreamElementParser(ElementParser parent, Uri baseUri) { super(parent, baseUri, TAG); @@ -496,16 +496,16 @@ public class SmoothStreamingManifestParser implements ManifestParser Date: Fri, 19 Sep 2014 18:34:05 +0100 Subject: [PATCH 016/110] Add WebVTT parser. --- .../exoplayer/text/webvtt/WebvttParser.java | 202 ++++++++++++++++++ .../exoplayer/text/webvtt/WebvttSubtitle.java | 93 ++++++++ 2 files changed, 295 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java create mode 100644 library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java new file mode 100644 index 0000000000..d3a560c2d9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java @@ -0,0 +1,202 @@ +/* + * 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.text.webvtt; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.text.SubtitleParser; +import com.google.android.exoplayer.util.MimeTypes; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple WebVTT parser. + *

+ * @see WebVTT specification + *

+ */ +public class WebvttParser implements SubtitleParser { + + /** + * This parser allows a custom header to be prepended to the WebVTT data, in the form of a text + * line starting with this string. + * + * @hide + */ + public static final String EXO_HEADER = "EXO-HEADER"; + /** + * A {@code OFFSET + value} element can be added to the custom header to specify an offset time + * (in microseconds) that should be subtracted from the embedded MPEGTS value. + * + * @hide + */ + public static final String OFFSET = "OFFSET:"; + + private static final long SAMPLING_RATE = 90; + + private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}"; + private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING); + private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+"); + private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+"); + + @Override + public WebvttSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs) + throws IOException { + ArrayList subtitles = new ArrayList(); + long mediaTimestampUs = startTimeUs; + long mediaTimestampOffsetUs = 0; + + BufferedReader webvttData = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME)); + String line; + + // file should start with "WEBVTT" on the first line or "EXO-HEADER" + line = webvttData.readLine(); + if (line == null) { + throw new ParserException("Expected WEBVTT or EXO-HEADER. Got null"); + } + if (line.startsWith(EXO_HEADER)) { + // parse the timestamp offset, if present + Matcher matcher = MEDIA_TIMESTAMP_OFFSET.matcher(line); + if (matcher.find()) { + mediaTimestampOffsetUs = Long.parseLong(matcher.group().substring(7)); + } + + // read the next line, which should now be WEBVTT + line = webvttData.readLine(); + if (line == null) { + throw new ParserException("Expected WEBVTT. Got null"); + } + } + if (!line.equals("WEBVTT")) { + throw new ParserException("Expected WEBVTT. Got " + line); + } + + // after "WEBVTT" there should be either an empty line or an "X-TIMESTAMP-MAP" line and then + // and empty line + line = webvttData.readLine(); + if (!line.isEmpty()) { + if (!line.startsWith("X-TIMESTAMP-MAP")) { + throw new ParserException("Expected an empty line or X-TIMESTAMP-MAP. Got " + line); + } + + // parse the media timestamp + Matcher matcher = MEDIA_TIMESTAMP.matcher(line); + if (!matcher.find()) { + throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestmap: " + line); + } else { + mediaTimestampUs = (Long.parseLong(matcher.group().substring(7)) * 1000) / SAMPLING_RATE + - mediaTimestampOffsetUs; + } + mediaTimestampUs = getAdjustedStartTime(mediaTimestampUs); + + // read in the next line (which should be an empty line) + line = webvttData.readLine(); + } + if (!line.isEmpty()) { + throw new ParserException("Expected an empty line after WEBVTT or X-TIMESTAMP-MAP. Got " + + line); + } + + // process the cues and text + while ((line = webvttData.readLine()) != null) { + // parse the cue timestamps + Matcher matcher = WEBVTT_TIMESTAMP.matcher(line); + long startTime; + long endTime; + String text = ""; + + // parse start timestamp + if (!matcher.find()) { + throw new ParserException("Expected cue start time: " + line); + } else { + startTime = parseTimestampUs(matcher.group()) + mediaTimestampUs; + } + + // parse end timestamp + if (!matcher.find()) { + throw new ParserException("Expected cue end time: " + line); + } else { + endTime = parseTimestampUs(matcher.group()) + mediaTimestampUs; + } + + // parse text + while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) { + text += line.trim() + "\n"; + } + + WebvttCue cue = new WebvttCue(startTime, endTime, text); + subtitles.add(cue); + } + + webvttData.close(); + inputStream.close(); + + // copy WebvttCue data into arrays for WebvttSubtitle constructor + String[] cueText = new String[subtitles.size()]; + long[] cueTimesUs = new long[2 * subtitles.size()]; + for (int subtitleIndex = 0; subtitleIndex < subtitles.size(); subtitleIndex++) { + int arrayIndex = subtitleIndex * 2; + WebvttCue cue = subtitles.get(subtitleIndex); + cueTimesUs[arrayIndex] = cue.startTime; + cueTimesUs[arrayIndex + 1] = cue.endTime; + cueText[subtitleIndex] = cue.text; + } + + WebvttSubtitle subtitle = new WebvttSubtitle(cueText, mediaTimestampUs, cueTimesUs); + return subtitle; + } + + @Override + public boolean canParse(String mimeType) { + return MimeTypes.TEXT_VTT.equals(mimeType); + } + + protected long getAdjustedStartTime(long startTimeUs) { + return startTimeUs; + } + + private static long parseTimestampUs(String s) throws NumberFormatException { + if (!s.matches(WEBVTT_TIMESTAMP_STRING)) { + throw new NumberFormatException("has invalid format"); + } + + String[] parts = s.split("\\.", 2); + long value = 0; + for (String group : parts[0].split(":")) { + value = value * 60 + Long.parseLong(group); + } + return (value * 1000 + Long.parseLong(parts[1])) * 1000; + } + + private static class WebvttCue { + public final long startTime; + public final long endTime; + public final String text; + + public WebvttCue(long startTime, long endTime, String text) { + this.startTime = startTime; + this.endTime = endTime; + this.text = text; + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java new file mode 100644 index 0000000000..0155636033 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java @@ -0,0 +1,93 @@ +/* + * 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.text.webvtt; + +import com.google.android.exoplayer.text.Subtitle; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; + +import java.util.Arrays; + +/** + * A representation of a WebVTT subtitle. + */ +public class WebvttSubtitle implements Subtitle { + + private final String[] cueText; + private final long startTimeUs; + private final long[] cueTimesUs; + private final long[] sortedCueTimesUs; + + /** + * @param cueText Text to be displayed during each cue. + * @param startTimeUs The start time of the subtitle. + * @param cueTimesUs Cue event times, where cueTimesUs[2 * i] and cueTimesUs[(2 * i) + 1] are + * the start and end times, respectively, corresponding to cueText[i]. + */ + public WebvttSubtitle(String[] cueText, long startTimeUs, long[] cueTimesUs) { + this.cueText = cueText; + this.startTimeUs = startTimeUs; + this.cueTimesUs = cueTimesUs; + this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); + Arrays.sort(sortedCueTimesUs); + } + + @Override + public long getStartTime() { + return startTimeUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + Assertions.checkArgument(timeUs >= 0); + int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false); + return index < sortedCueTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { + return sortedCueTimesUs.length; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < sortedCueTimesUs.length); + return sortedCueTimesUs[index]; + } + + @Override + public long getLastEventTime() { + if (getEventTimeCount() == 0) { + return -1; + } + return sortedCueTimesUs[sortedCueTimesUs.length - 1]; + } + + @Override + public String getText(long timeUs) { + StringBuilder subtitleStringBuilder = new StringBuilder(); + + for (int i = 0; i < cueTimesUs.length; i += 2) { + if ((cueTimesUs[i] <= timeUs) && (timeUs < cueTimesUs[i + 1])) { + subtitleStringBuilder.append(cueText[i / 2]); + } + } + + return (subtitleStringBuilder.length() > 0) ? subtitleStringBuilder.toString() : null; + } + +} From ce5eea72d2e2331a2d4057bf835b85da53f1a892 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:34:46 +0100 Subject: [PATCH 017/110] Auto-infer character encoding. --- .../com/google/android/exoplayer/text/TextTrackRenderer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 db5440ead4..4765a8e8b4 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.text; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; @@ -178,7 +177,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { resetSampleHolder = true; InputStream subtitleInputStream = new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size); - subtitle = subtitleParser.parse(subtitleInputStream, C.UTF8_NAME, sampleHolder.timeUs); + subtitle = subtitleParser.parse(subtitleInputStream, null, sampleHolder.timeUs); syncNextEventIndex(timeUs); textRendererNeedsUpdate = true; } else if (result == SampleSource.END_OF_STREAM) { From c4e1c3543c6857e897293e9f75aeeddd65830085 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Sep 2014 18:36:12 +0100 Subject: [PATCH 018/110] Enhance Loader API. --- .../exoplayer/chunk/ChunkSampleSource.java | 19 +++---- .../android/exoplayer/upstream/Loader.java | 51 ++++++++++++++----- 2 files changed, 47 insertions(+), 23 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 c59b321c9c..b20e6c3c66 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; import android.os.Handler; @@ -39,7 +40,7 @@ import java.util.List; * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a * {@link ChunkSource}. */ -public class ChunkSampleSource implements SampleSource, Loader.Listener { +public class ChunkSampleSource implements SampleSource, Loader.Callback { /** * Interface definition for a callback to be notified of {@link ChunkSampleSource} events. @@ -199,7 +200,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { @Override public boolean prepare() { Assertions.checkState(state == STATE_UNPREPARED); - loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType, this); + loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType); state = STATE_PREPARED; return true; } @@ -413,7 +414,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } @Override - public void onLoaded() { + public void onLoadCompleted(Loadable loadable) { Chunk currentLoadable = currentLoadableHolder.chunk; notifyLoadCompleted(currentLoadable.bytesLoaded()); try { @@ -436,7 +437,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } @Override - public void onCanceled() { + public void onLoadCanceled(Loadable loadable) { Chunk currentLoadable = currentLoadableHolder.chunk; notifyLoadCanceled(currentLoadable.bytesLoaded()); if (!isMediaChunk(currentLoadable)) { @@ -452,7 +453,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } @Override - public void onError(IOException e) { + public void onLoadError(Loadable loadable, IOException e) { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); @@ -553,7 +554,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { discardUpstreamMediaChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. - loader.startLoading(backedOffChunk); + loader.startLoading(backedOffChunk, this); } else { backedOffChunk.release(); maybeStartLoading(); @@ -564,7 +565,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { if (backedOffChunk == mediaChunks.getFirst()) { // We're not able to clear the first media chunk, so we have no choice but to continue // loading it. - loader.startLoading(backedOffChunk); + loader.startLoading(backedOffChunk, this); return; } @@ -579,7 +580,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. - loader.startLoading(backedOffChunk); + loader.startLoading(backedOffChunk, this); } else { // This call will remove and release at least one chunk from the end of mediaChunks. Since // the current loadable is the last media chunk, it is guaranteed to be removed. @@ -609,7 +610,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { notifyLoadStarted(currentLoadable.format.id, currentLoadable.trigger, true, -1, -1, currentLoadable.getLength()); } - loader.startLoading(currentLoadable); + loader.startLoading(currentLoadable, this); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java index fc232d328d..eb420c8f12 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.util.Log; @@ -72,22 +73,28 @@ public final class Loader { /** * Interface definition for a callback to be notified of {@link Loader} events. */ - public interface Listener { + public interface Callback { /** * Invoked when loading has been canceled. + * + * @param loadable The loadable whose load has been canceled. */ - void onCanceled(); + void onLoadCanceled(Loadable loadable); /** * Invoked when the data source has been fully loaded. + * + * @param loadable The loadable whose load has completed. */ - void onLoaded(); + void onLoadCompleted(Loadable loadable); /** * Invoked when the data source is stopped due to an error. + * + * @param loadable The loadable whose load has failed. */ - void onError(IOException exception); + void onLoadError(Loadable loadable, IOException exception); } @@ -95,18 +102,29 @@ public final class Loader { private static final int MSG_ERROR = 1; private final ExecutorService downloadExecutorService; - private final Listener listener; private LoadTask currentTask; private boolean loading; /** * @param threadName A name for the loader's thread. - * @param listener A listener to invoke when state changes occur. */ - public Loader(String threadName, Listener listener) { + public Loader(String threadName) { this.downloadExecutorService = Util.newSingleThreadExecutor(threadName); - this.listener = listener; + } + + /** + * Invokes {@link #startLoading(Looper, Loadable, Callback)}, using the {@link Looper} + * associated with the calling thread. + * + * @param loadable The {@link Loadable} to load. + * @param callback A callback to invoke when the load ends. + * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. + */ + public void startLoading(Loadable loadable, Callback callback) { + Looper myLooper = Looper.myLooper(); + Assertions.checkState(myLooper != null); + startLoading(myLooper, loadable, callback); } /** @@ -115,12 +133,14 @@ public final class Loader { * A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method * must not be called when another load is in progress. * + * @param looper The looper of the thread on which the callback should be invoked. * @param loadable The {@link Loadable} to load. + * @param callback A callback to invoke when the load ends. */ - public void startLoading(Loadable loadable) { + public void startLoading(Looper looper, Loadable loadable, Callback callback) { Assertions.checkState(!loading); loading = true; - currentTask = new LoadTask(loadable); + currentTask = new LoadTask(looper, loadable, callback); downloadExecutorService.submit(currentTask); } @@ -161,11 +181,14 @@ public final class Loader { private static final String TAG = "LoadTask"; private final Loadable loadable; + private final Loader.Callback callback; private volatile Thread executorThread; - public LoadTask(Loadable loadable) { + public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) { + super(looper); this.loadable = loadable; + this.callback = callback; } public void quit() { @@ -200,15 +223,15 @@ public final class Loader { public void handleMessage(Message msg) { onFinished(); if (loadable.isLoadCanceled()) { - listener.onCanceled(); + callback.onLoadCanceled(loadable); return; } switch (msg.what) { case MSG_END_OF_SOURCE: - listener.onLoaded(); + callback.onLoadCompleted(loadable); break; case MSG_ERROR: - listener.onError((IOException) msg.obj); + callback.onLoadError(loadable, (IOException) msg.obj); break; } } From 4fdd68facf9861dfbdf48d9c86c1a53a361b3f54 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 23 Sep 2014 11:13:09 +0100 Subject: [PATCH 019/110] Fix SmoothStreamingManifest to handle large timestamps. --- .../SmoothStreamingChunkSource.java | 2 +- .../SmoothStreamingManifest.java | 76 +++++++++++-------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index a6af466727..74ee50925e 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -77,7 +77,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex, int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { this.streamElement = manifest.streamElements[streamElementIndex]; - this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.getDurationUs()); + this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.durationUs); this.dataSource = dataSource; this.formatEvaluator = formatEvaluator; this.evaluation = new Evaluation(); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 08a8f0a7b1..a26ca6a48e 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -31,6 +31,8 @@ import java.util.UUID; */ public class SmoothStreamingManifest { + private static final long MICROS_PER_SECOND = 1000000L; + public final int majorVersion; public final int minorVersion; public final long timescale; @@ -38,9 +40,8 @@ public class SmoothStreamingManifest { public final boolean isLive; public final ProtectionElement protectionElement; public final StreamElement[] streamElements; - - private final long duration; - private final long dvrWindowLength; + public final long durationUs; + public final long dvrWindowLengthUs; public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration, long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, @@ -48,33 +49,23 @@ public class SmoothStreamingManifest { this.majorVersion = majorVersion; this.minorVersion = minorVersion; this.timescale = timescale; - this.duration = duration; - this.dvrWindowLength = dvrWindowLength; this.lookAheadCount = lookAheadCount; this.isLive = isLive; this.protectionElement = protectionElement; this.streamElements = streamElements; - } - - /** - * Gets the duration of the media. - *

- * For a live presentation the duration may be an approximation of the eventual final duration, - * or 0 if an approximate duration is not known. - * - * @return The duration of the media in microseconds. - */ - public long getDurationUs() { - return (duration * 1000000L) / timescale; - } - - /** - * Gets the DVR window length, or 0 if no window length was specified. - * - * @return The duration of the DVR window in microseconds, or 0 if no window length was specified. - */ - public long getDvrWindowLengthUs() { - return (dvrWindowLength * 1000000L) / timescale; + if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) { + long divisionFactor = timescale / MICROS_PER_SECOND; + dvrWindowLengthUs = dvrWindowLength / divisionFactor; + durationUs = duration / divisionFactor; + } else if (timescale < MICROS_PER_SECOND && (MICROS_PER_SECOND % timescale) == 0) { + long multiplicationFactor = MICROS_PER_SECOND / timescale; + dvrWindowLengthUs = dvrWindowLength * multiplicationFactor; + durationUs = duration * multiplicationFactor; + } else { + double multiplicationFactor = (double) MICROS_PER_SECOND / timescale; + dvrWindowLengthUs = (long) (dvrWindowLength * multiplicationFactor); + durationUs = (long) (duration * multiplicationFactor); + } } /** @@ -173,7 +164,8 @@ public class SmoothStreamingManifest { private final String chunkTemplate; private final List chunkStartTimes; - private final long lastChunkDuration; + private final long[] chunkStartTimesUs; + private final long lastChunkDurationUs; public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, @@ -194,7 +186,26 @@ public class SmoothStreamingManifest { this.tracks = tracks; this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; - this.lastChunkDuration = lastChunkDuration; + chunkStartTimesUs = new long[chunkStartTimes.size()]; + if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) { + long divisionFactor = timescale / MICROS_PER_SECOND; + for (int i = 0; i < chunkStartTimesUs.length; i++) { + chunkStartTimesUs[i] = chunkStartTimes.get(i) / divisionFactor; + } + lastChunkDurationUs = lastChunkDuration / divisionFactor; + } else if (timescale < MICROS_PER_SECOND && (MICROS_PER_SECOND % timescale) == 0) { + long multiplicationFactor = MICROS_PER_SECOND / timescale; + for (int i = 0; i < chunkStartTimesUs.length; i++) { + chunkStartTimesUs[i] = chunkStartTimes.get(i) * multiplicationFactor; + } + lastChunkDurationUs = lastChunkDuration * multiplicationFactor; + } else { + double multiplicationFactor = (double) MICROS_PER_SECOND / timescale; + for (int i = 0; i < chunkStartTimesUs.length; i++) { + chunkStartTimesUs[i] = (long) (chunkStartTimes.get(i) * multiplicationFactor); + } + lastChunkDurationUs = (long) (lastChunkDuration * multiplicationFactor); + } } /** @@ -204,7 +215,7 @@ public class SmoothStreamingManifest { * @return The index of the corresponding chunk. */ public int getChunkIndex(long timeUs) { - return Util.binarySearchFloor(chunkStartTimes, (timeUs * timescale) / 1000000L, true, true); + return Util.binarySearchFloor(chunkStartTimesUs, timeUs, true, true); } /** @@ -214,7 +225,7 @@ public class SmoothStreamingManifest { * @return The start time of the chunk, in microseconds. */ public long getStartTimeUs(int chunkIndex) { - return (chunkStartTimes.get(chunkIndex) * 1000000L) / timescale; + return chunkStartTimesUs[chunkIndex]; } /** @@ -224,9 +235,8 @@ public class SmoothStreamingManifest { * @return The duration of the chunk, in microseconds. */ public long getChunkDurationUs(int chunkIndex) { - long chunkDuration = (chunkIndex == chunkCount - 1) ? lastChunkDuration - : chunkStartTimes.get(chunkIndex + 1) - chunkStartTimes.get(chunkIndex); - return chunkDuration / timescale; + return (chunkIndex == chunkCount - 1) ? lastChunkDurationUs + : chunkStartTimesUs[chunkIndex + 1] - chunkStartTimesUs[chunkIndex]; } /** From 7cb55e23f6877fccdc376b705c16e8eb8b051c36 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 23 Sep 2014 11:13:31 +0100 Subject: [PATCH 020/110] Correctly handle zero length fragmentRun. --- .../android/exoplayer/parser/mp4/FragmentedMp4Extractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ede9e93b2..0fc1e1b200 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 @@ -246,7 +246,7 @@ public final class FragmentedMp4Extractor implements Extractor { @Override public boolean seekTo(long seekTimeUs, boolean allowNoop) { pendingSeekTimeMs = (int) (seekTimeUs / 1000); - if (allowNoop && fragmentRun != null + if (allowNoop && fragmentRun != null && fragmentRun.length > 0 && pendingSeekTimeMs >= fragmentRun.getSamplePresentationTime(0) && pendingSeekTimeMs <= fragmentRun.getSamplePresentationTime(fragmentRun.length - 1)) { int sampleIndexFound = 0; From 1ed048dba80f4b3e2e9e8da8aff4578f2e24b0d1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 23 Sep 2014 11:13:54 +0100 Subject: [PATCH 021/110] Clean up TTML timestamp parsing. --- .../android/exoplayer/text/ttml/TtmlParser.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index 6fdf7d546f..2fd1850f53 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -233,22 +233,22 @@ public class TtmlParser implements SubtitleParser { matcher = OFFSET_TIME.matcher(time); if (matcher.matches()) { String timeValue = matcher.group(1); - double value = Double.parseDouble(timeValue); + double offsetSeconds = Double.parseDouble(timeValue); String unit = matcher.group(2); if (unit.equals("h")) { - value *= 3600L * 1000000L; + offsetSeconds *= 3600; } else if (unit.equals("m")) { - value *= 60 * 1000000; + offsetSeconds *= 60; } else if (unit.equals("s")) { - value *= 1000000; + // Do nothing. } else if (unit.equals("ms")) { - value *= 1000; + offsetSeconds /= 1000; } else if (unit.equals("f")) { - value = value / frameRate * 1000000; + offsetSeconds /= frameRate; } else if (unit.equals("t")) { - value = value / tickRate * 1000000; + offsetSeconds /= tickRate; } - return (long) value; + return (long) (offsetSeconds * 1000000); } throw new NumberFormatException("Malformed time expression: " + time); } From f82a331728650ef8f40aa93e00c493f8902a9b58 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 23 Sep 2014 11:14:16 +0100 Subject: [PATCH 022/110] Fix StreamingDrmSessionmanager. Use locally bound variable instead of class variable. --- .../android/exoplayer/drm/StreamingDrmSessionManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index e914a8777f..8d4e697d4d 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -284,13 +284,13 @@ public class StreamingDrmSessionManager implements DrmSessionManager { } } - private void onError(Exception e) { + private void onError(final Exception e) { lastException = e; if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onDrmSessionManagerError(lastException); + eventListener.onDrmSessionManagerError(e); } }); } From da125bb5ccd67c94dcc9c82d0d1754802b222fdf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 23 Sep 2014 11:17:36 +0100 Subject: [PATCH 023/110] Merge DashLiveMpdFetcher logic into generic ManifestFetcher. This allows ManifestFetcher to both execute the initial manifest load and be plugged into an ExoPlayer ChunkSource, where it can be used for repeated manfiest refreshes during live playback. --- .../full/player/DashVodRendererBuilder.java | 10 +- .../SmoothStreamingRendererBuilder.java | 13 +- .../demo/simple/DashVodRendererBuilder.java | 10 +- .../SmoothStreamingRendererBuilder.java | 7 +- .../exoplayer/util/ManifestFetcher.java | 297 ++++++++++++++---- 5 files changed, 260 insertions(+), 77 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 966ce7a43b..32d60beafc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -52,11 +52,11 @@ import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.UnsupportedSchemeException; -import android.os.AsyncTask; import android.os.Handler; import android.util.Pair; import android.widget.TextView; +import java.io.IOException; import java.util.ArrayList; /** @@ -96,13 +96,13 @@ public class DashVodRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher mpdFetcher = - new ManifestFetcher(parser, this); - mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + ManifestFetcher manifestFetcher = + new ManifestFetcher(parser, contentId, url); + manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { + public void onManifestError(String contentId, IOException e) { callback.onRenderersError(e); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index af806e9fe7..a5cb7093e4 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -43,7 +43,6 @@ import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.HttpDataSource; import com.google.android.exoplayer.util.ManifestFetcher; -import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; @@ -52,6 +51,7 @@ import android.media.UnsupportedSchemeException; import android.os.Handler; import android.widget.TextView; +import java.io.IOException; import java.util.ArrayList; import java.util.UUID; @@ -59,7 +59,7 @@ import java.util.UUID; * A {@link RendererBuilder} for SmoothStreaming. */ public class SmoothStreamingRendererBuilder implements RendererBuilder, - ManifestCallback { + ManifestFetcher.ManifestCallback { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; @@ -88,16 +88,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; - SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, this); - manifestFetcher.execute(url + "/Manifest", contentId); + new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { - callback.onRenderersError(e); + public void onManifestError(String contentId, IOException exception) { + callback.onRenderersError(exception); } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index cc994543b9..4428fba201 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -42,9 +42,9 @@ import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; -import android.os.AsyncTask; import android.os.Handler; +import java.io.IOException; import java.util.ArrayList; /** @@ -76,13 +76,13 @@ import java.util.ArrayList; public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher mpdFetcher = - new ManifestFetcher(parser, this); - mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + ManifestFetcher manifestFetcher = + new ManifestFetcher(parser, contentId, url); + manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { + public void onManifestError(String contentId, IOException e) { callback.onRenderersError(e); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index efde2de096..f936b19219 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; import android.os.Handler; +import java.io.IOException; import java.util.ArrayList; /** @@ -74,12 +75,12 @@ import java.util.ArrayList; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, this); - manifestFetcher.execute(url + "/Manifest", contentId); + new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { + public void onManifestError(String contentId, IOException e) { callback.onRenderersError(e); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 804b1240e7..423b1d0204 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -15,114 +15,297 @@ */ package com.google.android.exoplayer.util; -import android.net.Uri; -import android.os.AsyncTask; +import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.Loader.Loadable; + +import android.os.Looper; +import android.os.SystemClock; +import android.util.Pair; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.concurrent.CancellationException; /** - * An {@link AsyncTask} for loading and parsing media manifests. + * Performs both single and repeated loads of media manfifests. * - * @param The type of the manifest being parsed. + * @param The type of manifest. */ -public class ManifestFetcher extends AsyncTask { +public class ManifestFetcher implements Loader.Callback { /** - * Invoked with the result of a manifest fetch. + * Callback for the result of a single load. * - * @param The type of the manifest being parsed. + * @param The type of manifest. */ public interface ManifestCallback { /** - * Invoked from {@link #onPostExecute(Object)} with the parsed manifest. + * Invoked when the load has successfully completed. * * @param contentId The content id of the media. - * @param manifest The parsed manifest. + * @param manifest The loaded manifest. */ void onManifest(String contentId, T manifest); /** - * Invoked from {@link #onPostExecute(Object)} if an error occurred. + * Invoked when the load has failed. * * @param contentId The content id of the media. - * @param e The error. + * @param e The cause of the failure. */ - void onManifestError(String contentId, Exception e); + void onManifestError(String contentId, IOException e); } - public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000; + /* package */ final ManifestParser parser; + /* package */ final String manifestUrl; + /* package */ final String contentId; - private final ManifestParser parser; - private final ManifestCallback callback; - private final int timeoutMillis; + private int enabledCount; + private Loader loader; + private ManifestLoadable currentLoadable; - private volatile String contentId; - private volatile Exception exception; + private int loadExceptionCount; + private long loadExceptionTimestamp; + private IOException loadException; + + private volatile T manifest; + private volatile long manifestLoadTimestamp; /** - * @param callback The callback to provide with the parsed manifest (or error). + * @param parser A parser to parse the loaded manifest data. + * @param contentId The content id of the content being loaded. May be null. + * @param manifestUrl The manifest location. */ - public ManifestFetcher(ManifestParser parser, ManifestCallback callback) { - this(parser, callback, DEFAULT_HTTP_TIMEOUT_MILLIS); - } - - /** - * @param parser Parses the manifest from the loaded data. - * @param callback The callback to provide with the parsed manifest (or error). - * @param timeoutMillis The timeout in milliseconds for the connection used to load the data. - */ - public ManifestFetcher(ManifestParser parser, ManifestCallback callback, - int timeoutMillis) { + public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl) { this.parser = parser; - this.callback = callback; - this.timeoutMillis = timeoutMillis; + this.contentId = contentId; + this.manifestUrl = manifestUrl; + } + + /** + * Performs a single manifest load. + * + * @param callbackLooper The looper associated with the thread on which the callback should be + * invoked. + * @param callback The callback to receive the result. + */ + public void singleLoad(Looper callbackLooper, final ManifestCallback callback) { + SingleFetchHelper fetchHelper = new SingleFetchHelper(callbackLooper, callback); + fetchHelper.startLoading(); + } + + /** + * Gets a {@link Pair} containing the most recently loaded manifest together with the timestamp + * at which the load completed. + * + * @return The most recently loaded manifest and the timestamp at which the load completed, or + * null if no manifest has loaded. + */ + public T getManifest() { + return manifest; + } + + /** + * Gets the value of {@link SystemClock#elapsedRealtime()} when the last load completed. + * + * @return The value of {@link SystemClock#elapsedRealtime()} when the last load completed. + */ + public long getManifestLoadTimestamp() { + return manifestLoadTimestamp; + } + + /** + * Gets the error that affected the most recent attempt to load the manifest, or null if the + * most recent attempt was successful. + * + * @return The error, or null if the most recent attempt was successful. + */ + public IOException getError() { + if (loadExceptionCount <= 1) { + // Don't report an exception until at least 1 retry attempt has been made. + return null; + } + return loadException; + } + + /** + * Enables refresh functionality. + */ + public void enable() { + if (enabledCount++ == 0) { + loadExceptionCount = 0; + loadException = null; + } + } + + /** + * Disables refresh functionality. + */ + public void disable() { + if (--enabledCount == 0) { + if (loader != null) { + loader.release(); + loader = null; + } + } + } + + /** + * Should be invoked repeatedly by callers who require an updated manifest. + */ + public void requestRefresh() { + if (loadException != null && SystemClock.elapsedRealtime() + < (loadExceptionTimestamp + getRetryDelayMillis(loadExceptionCount))) { + // The previous load failed, and it's too soon to try again. + return; + } + if (loader == null) { + loader = new Loader("manifestLoader"); + } + if (!loader.isLoading()) { + currentLoadable = new ManifestLoadable(); + loader.startLoading(currentLoadable, this); + } } @Override - protected final T doInBackground(String... data) { - try { - contentId = data.length > 1 ? data[1] : null; - String urlString = data[0]; + public void onLoadCompleted(Loadable loadable) { + if (currentLoadable != loadable) { + // Stale event. + return; + } + + manifest = currentLoadable.result; + manifestLoadTimestamp = SystemClock.elapsedRealtime(); + loadExceptionCount = 0; + loadException = null; + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // Do nothing. + } + + @Override + public void onLoadError(Loadable loadable, IOException exception) { + if (currentLoadable != loadable) { + // Stale event. + return; + } + + loadExceptionCount++; + loadExceptionTimestamp = SystemClock.elapsedRealtime(); + loadException = new IOException(exception); + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + private class SingleFetchHelper implements Loader.Callback { + + private final Looper callbackLooper; + private final ManifestCallback wrappedCallback; + private final Loader singleUseLoader; + private final ManifestLoadable singleUseLoadable; + + public SingleFetchHelper(Looper callbackLooper, ManifestCallback wrappedCallback) { + this.callbackLooper = callbackLooper; + this.wrappedCallback = wrappedCallback; + singleUseLoader = new Loader("manifestLoader:single"); + singleUseLoadable = new ManifestLoadable(); + } + + public void startLoading() { + singleUseLoader.startLoading(callbackLooper, singleUseLoadable, this); + } + + @Override + public void onLoadCompleted(Loadable loadable) { + try { + manifest = singleUseLoadable.result; + manifestLoadTimestamp = SystemClock.elapsedRealtime(); + wrappedCallback.onManifest(contentId, singleUseLoadable.result); + } finally { + releaseLoader(); + } + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // This shouldn't ever happen, but handle it anyway. + try { + IOException exception = new IOException("Load cancelled", new CancellationException()); + wrappedCallback.onManifestError(contentId, exception); + } finally { + releaseLoader(); + } + } + + @Override + public void onLoadError(Loadable loadable, IOException exception) { + try { + wrappedCallback.onManifestError(contentId, exception); + } finally { + releaseLoader(); + } + } + + private void releaseLoader() { + singleUseLoader.release(); + } + + } + + private class ManifestLoadable implements Loadable { + + private static final int TIMEOUT_MILLIS = 10000; + + /* package */ volatile T result; + private volatile boolean isCanceled; + + @Override + public void cancelLoad() { + // We don't actually cancel anything, but we need to record the cancellation so that + // isLoadCanceled can return the correct value. + isCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return isCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { String inputEncoding = null; InputStream inputStream = null; try { - Uri baseUri = Util.parseBaseUri(urlString); - HttpURLConnection connection = configureHttpConnection(new URL(urlString)); + HttpURLConnection connection = configureHttpConnection(new URL(manifestUrl)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - return parser.parse(inputStream, inputEncoding, contentId, baseUri); + result = parser.parse(inputStream, inputEncoding, contentId, + Util.parseBaseUri(manifestUrl)); } finally { if (inputStream != null) { inputStream.close(); } } - } catch (Exception e) { - exception = e; - return null; } - } - @Override - protected final void onPostExecute(T manifest) { - if (exception != null) { - callback.onManifestError(contentId, exception); - } else { - callback.onManifest(contentId, manifest); + private HttpURLConnection configureHttpConnection(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(TIMEOUT_MILLIS); + connection.setReadTimeout(TIMEOUT_MILLIS); + connection.setDoOutput(false); + connection.connect(); + return connection; } - } - private HttpURLConnection configureHttpConnection(URL url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(timeoutMillis); - connection.setReadTimeout(timeoutMillis); - connection.setDoOutput(false); - connection.connect(); - return connection; } } From fc230733aed9ec2f17764da16cff45cb163a364b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 23 Sep 2014 11:21:44 +0100 Subject: [PATCH 024/110] Ignore blockingSendMessage calls after release. Previously we'd end up blocking forever in this case, which is the worst thing we could do :). We could either throw an exception or just print a warning. Printing a warning is more in line with what other methods do (Handler prints a "sending message to dead thread" warning). --- .../exoplayer/ExoPlayerImplInternal.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) 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 1aaac4ebcf..0184ea9956 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -158,6 +158,10 @@ import java.util.List; public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message) { + if (released) { + Log.w(TAG, "Sent message(" + messageType + ") after release. Message ignored."); + return; + } int messageNumber = customMessagesSent++; handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget(); while (customMessagesProcessed <= messageNumber) { @@ -170,17 +174,18 @@ import java.util.List; } public synchronized void release() { - if (!released) { - handler.sendEmptyMessage(MSG_RELEASE); - while (!released) { - try { - wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - internalPlaybackThread.quit(); + if (released) { + return; } + handler.sendEmptyMessage(MSG_RELEASE); + while (!released) { + try { + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + internalPlaybackThread.quit(); } @Override From 4adf8f77f49fa1d7f21f3629e68fc94b6c5f9c4a Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 25 Sep 2014 20:13:40 +0100 Subject: [PATCH 025/110] Tweak stop/disable cycles. --- .../MediaCodecAudioTrackRenderer.java | 9 ++++++--- .../MediaCodecVideoTrackRenderer.java | 4 ++-- .../exoplayer/chunk/ChunkSampleSource.java | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 13 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 a43c405dc7..8a75331465 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -417,11 +417,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onStopped() { - super.onStopped(); if (audioTrack != null) { resetSyncParams(); audioTrack.pause(); } + super.onStopped(); } @Override @@ -563,9 +563,12 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onDisabled() { - super.onDisabled(); - releaseAudioTrack(); audioSessionId = 0; + try { + releaseAudioTrack(); + } finally { + super.onDisabled(); + } } @Override 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 4a6dcc8206..565ab41723 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -269,20 +269,20 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onStopped() { - super.onStopped(); joiningDeadlineUs = -1; notifyAndResetDroppedFrameCount(); + super.onStopped(); } @Override public void onDisabled() { - super.onDisabled(); currentWidth = -1; currentHeight = -1; currentPixelWidthHeightRatio = -1; lastReportedWidth = -1; lastReportedHeight = -1; lastReportedPixelWidthHeightRatio = -1; + super.onDisabled(); } @Override 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 b20e6c3c66..f2f0ce031a 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 @@ -238,14 +238,17 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { Assertions.checkState(track == 0); pendingDiscontinuity = false; state = STATE_PREPARED; - loadControl.unregister(this); - chunkSource.disable(mediaChunks); - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - clearMediaChunks(); - clearCurrentLoadable(); - loadControl.trimAllocator(); + try { + chunkSource.disable(mediaChunks); + } finally { + loadControl.unregister(this); + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearMediaChunks(); + clearCurrentLoadable(); + loadControl.trimAllocator(); + } } } From dd30632aa1522b47d14b0ad9aaad45c5ccaf4b19 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 25 Sep 2014 20:15:59 +0100 Subject: [PATCH 026/110] SmoothStreaming Live support. Issue: #12 --- .../android/exoplayer/demo/DemoUtil.java | 2 +- .../android/exoplayer/demo/Samples.java | 6 +- .../demo/full/FullPlayerActivity.java | 2 +- .../SmoothStreamingRendererBuilder.java | 19 +- .../SmoothStreamingRendererBuilder.java | 16 +- .../exoplayer/BehindLiveWindowException.java | 33 ++++ .../SmoothStreamingChunkSource.java | 183 +++++++++++++++--- 7 files changed, 220 insertions(+), 41 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index f8ceebc600..6479b28a7e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -45,7 +45,7 @@ public class DemoUtil { public static final String CONTENT_ID_EXTRA = "content_id"; public static final int TYPE_DASH_VOD = 0; - public static final int TYPE_SS_VOD = 1; + public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 39b21b62bf..93d08af4cc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -56,7 +56,7 @@ package com.google.android.exoplayer.demo; false), new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS_VOD, false, false), + DemoUtil.TYPE_SS, false, false), new Sample("Dizzy (Misc)", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false), }; @@ -92,10 +92,10 @@ package com.google.android.exoplayer.demo; public static final Sample[] SMOOTHSTREAMING = new Sample[] { new Sample("Super speed", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS_VOD, false, true), + DemoUtil.TYPE_SS, false, true), new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS_VOD, true, true), + DemoUtil.TYPE_SS, true, true), }; public static final Sample[] WIDEVINE_GTS = new Sample[] { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 48ed2f5ded..9966124ced 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -167,7 +167,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private RendererBuilder getRendererBuilder() { String userAgent = DemoUtil.getUserAgent(this); switch (contentType) { - case DemoUtil.TYPE_SS_VOD: + case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, new SmoothStreamingTestMediaDrmCallback(), debugTextView); case DemoUtil.TYPE_DASH_VOD: diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index a5cb7093e4..940691b48c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -65,6 +65,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; private static final int TTML_BUFFER_SEGMENTS = 2; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private final String userAgent; private final String url; @@ -74,6 +75,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private DemoPlayer player; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId, MediaDrmCallback drmCallback, TextView debugTextView) { @@ -89,8 +91,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher = new ManifestFetcher(parser, contentId, + url + "/Manifest"); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -154,9 +156,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, videoStreamElementIndex, videoTrackIndices, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter)); + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); @@ -181,8 +183,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, for (int i = 0; i < manifest.streamElements.length; i++) { if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) { audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name; - audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(manifest, - i, new int[] {0}, audioDataSource, audioFormatEvaluator); + audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource( + manifestFetcher, i, new int[] {0}, audioDataSource, audioFormatEvaluator, + LIVE_EDGE_LATENCY_MS); audioStreamElementCount++; } } @@ -211,8 +214,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, for (int i = 0; i < manifest.streamElements.length; i++) { if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) { textTrackNames[textStreamElementCount] = manifest.streamElements[i].language; - textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifest, - i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator); + textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifestFetcher, + i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator, LIVE_EDGE_LATENCY_MS); textStreamElementCount++; } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index f936b19219..eb80499ebd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -54,6 +54,7 @@ import java.util.ArrayList; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private final SimplePlayerActivity playerActivity; private final String userAgent; @@ -61,6 +62,7 @@ import java.util.ArrayList; private final String contentId; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; public SmoothStreamingRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, String contentId) { @@ -74,8 +76,8 @@ import java.util.ArrayList; public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher = new ManifestFetcher(parser, contentId, + url + "/Manifest"); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @@ -120,8 +122,9 @@ import java.util.ArrayList; // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex, - videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + videoStreamElementIndex, videoTrackIndices, videoDataSource, + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, @@ -129,8 +132,9 @@ import java.util.ArrayList; // Build the audio renderer. DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifest, audioStreamElementIndex, - new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator()); + ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + audioStreamElementIndex, new int[] {0}, audioDataSource, + new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( diff --git a/library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java b/library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java new file mode 100644 index 0000000000..074a1de01a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java @@ -0,0 +1,33 @@ +/* + * 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; + +import java.io.IOException; + +/** + * Thrown when a live playback falls behind the available media window. + */ +public class BehindLiveWindowException extends IOException { + + public BehindLiveWindowException() { + super(); + } + + public BehindLiveWindowException(String message) { + super(message); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 74ee50925e..2b676e6b52 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.smoothstreaming; +import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.chunk.Chunk; @@ -36,8 +37,10 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Trac import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.ManifestFetcher; import android.net.Uri; +import android.os.SystemClock; import android.util.Base64; import android.util.SparseArray; @@ -51,13 +54,16 @@ import java.util.List; */ public class SmoothStreamingChunkSource implements ChunkSource { + private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000; private static final int INITIALIZATION_VECTOR_SIZE = 8; - private final StreamElement streamElement; + private final ManifestFetcher manifestFetcher; + private final int streamElementIndex; private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator formatEvaluator; private final Evaluation evaluation; + private final long liveEdgeLatencyUs; private final int maxWidth; private final int maxHeight; @@ -65,7 +71,42 @@ public class SmoothStreamingChunkSource implements ChunkSource { private final SparseArray extractors; private final SmoothStreamingFormat[] formats; + private SmoothStreamingManifest currentManifest; + private int currentManifestChunkOffset; + private boolean finishedCurrentManifest; + + private IOException fatalError; + /** + * Constructor to use for live streaming. + *

+ * May also be used for fixed duration content, in which case the call is equivalent to calling + * the other constructor, passing {@code manifestFetcher.getManifest()} is the first argument. + * + * @param manifestFetcher A fetcher for the manifest, which must have already successfully + * completed an initial load. + * @param streamElementIndex The index of the stream element in the manifest to be provided by + * the source. + * @param trackIndices The indices of the tracks within the stream element to be considered by + * the source. May be null if all tracks within the element should be considered. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should + * lag behind the "live edge" (i.e. the end of the most recently defined media in the + * manifest). Choosing a small value will minimize latency introduced by the player, however + * note that the value sets an upper bound on the length of media that the player can buffer. + * Hence a small value may increase the probability of rebuffering and playback failures. + */ + public SmoothStreamingChunkSource(ManifestFetcher manifestFetcher, + int streamElementIndex, int[] trackIndices, DataSource dataSource, + FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { + this(manifestFetcher, manifestFetcher.getManifest(), streamElementIndex, trackIndices, + dataSource, formatEvaluator, liveEdgeLatencyMs); + } + + /** + * Constructor to use for fixed duration content. + * * @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}. * @param streamElementIndex The index of the stream element in the manifest to be provided by * the source. @@ -76,14 +117,25 @@ public class SmoothStreamingChunkSource implements ChunkSource { */ public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex, int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { - this.streamElement = manifest.streamElements[streamElementIndex]; - this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.durationUs); + this(null, manifest, streamElementIndex, trackIndices, dataSource, formatEvaluator, 0); + } + + private SmoothStreamingChunkSource(ManifestFetcher manifestFetcher, + SmoothStreamingManifest initialManifest, int streamElementIndex, int[] trackIndices, + DataSource dataSource, FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { + this.manifestFetcher = manifestFetcher; + this.streamElementIndex = streamElementIndex; + this.currentManifest = initialManifest; this.dataSource = dataSource; this.formatEvaluator = formatEvaluator; - this.evaluation = new Evaluation(); + this.liveEdgeLatencyUs = liveEdgeLatencyMs * 1000; + + StreamElement streamElement = getElement(initialManifest); + trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, initialManifest.durationUs); + evaluation = new Evaluation(); TrackEncryptionBox[] trackEncryptionBoxes = null; - ProtectionElement protectionElement = manifest.protectionElement; + ProtectionElement protectionElement = initialManifest.protectionElement; if (protectionElement != null) { byte[] keyId = getKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[1]; @@ -135,22 +187,52 @@ public class SmoothStreamingChunkSource implements ChunkSource { @Override public void enable() { - // Do nothing. + fatalError = null; + if (manifestFetcher != null) { + manifestFetcher.enable(); + } } @Override public void disable(List queue) { - // Do nothing. + if (manifestFetcher != null) { + manifestFetcher.disable(); + } } @Override public void continueBuffering(long playbackPositionUs) { - // Do nothing + if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { + return; + } + + SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); + if (currentManifest != newManifest && newManifest != null) { + StreamElement currentElement = getElement(currentManifest); + StreamElement newElement = getElement(newManifest); + if (newElement.chunkCount == 0) { + currentManifestChunkOffset += currentElement.chunkCount; + } else if (currentElement.chunkCount > 0) { + currentManifestChunkOffset += currentElement.getChunkIndex(newElement.getStartTimeUs(0)); + } + currentManifest = newManifest; + finishedCurrentManifest = false; + } + + if (finishedCurrentManifest && (SystemClock.elapsedRealtime() + > manifestFetcher.getManifestLoadTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { + manifestFetcher.requestRefresh(); + } } @Override public final void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, ChunkOperationHolder out) { + if (fatalError != null) { + out.chunk = null; + return; + } + evaluation.queueSize = queue.size(); formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation); SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format; @@ -166,30 +248,63 @@ public class SmoothStreamingChunkSource implements ChunkSource { return; } - int nextChunkIndex; - if (queue.isEmpty()) { - nextChunkIndex = streamElement.getChunkIndex(seekPositionUs); - } else { - nextChunkIndex = queue.get(out.queueSize - 1).nextChunkIndex; - } + // In all cases where we return before instantiating a new chunk at the bottom of this method, + // we want out.chunk to be null. + out.chunk = null; - if (nextChunkIndex == -1) { - out.chunk = null; + StreamElement streamElement = getElement(currentManifest); + if (streamElement.chunkCount == 0) { + // The manifest is currently empty for this stream. + finishedCurrentManifest = true; return; } - boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1; - Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, nextChunkIndex); + int chunkIndex; + if (queue.isEmpty()) { + if (currentManifest.isLive) { + seekPositionUs = getLiveSeekPosition(); + } + chunkIndex = streamElement.getChunkIndex(seekPositionUs); + } else { + chunkIndex = queue.get(out.queueSize - 1).nextChunkIndex - currentManifestChunkOffset; + } + + if (currentManifest.isLive) { + if (chunkIndex < 0) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } else if (chunkIndex >= streamElement.chunkCount) { + // This is beyond the last chunk in the current manifest. + finishedCurrentManifest = true; + return; + } else if (chunkIndex == streamElement.chunkCount - 1) { + // This is the last chunk in the current manifest. Mark the manifest as being finished, + // but continue to return the final chunk. + finishedCurrentManifest = true; + } + } else if (chunkIndex == -1) { + // We've reached the end of the stream. + return; + } + + boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1; + long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); + long nextChunkStartTimeUs = isLastChunk ? -1 + : chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); + int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; + + Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, - extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex, - isLastChunk, streamElement.getStartTimeUs(nextChunkIndex), - isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0); + extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, currentAbsoluteChunkIndex, + isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); out.chunk = mediaChunk; } @Override public IOException getError() { - return null; + return fatalError != null ? fatalError + : (manifestFetcher != null ? manifestFetcher.getError() : null); } @Override @@ -197,6 +312,30 @@ public class SmoothStreamingChunkSource implements ChunkSource { // Do nothing. } + /** + * For live playbacks, determines the seek position that snaps playback to be + * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest + * + * @return The seek position in microseconds. + */ + private long getLiveSeekPosition() { + long liveEdgeTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < currentManifest.streamElements.length; i++) { + StreamElement streamElement = currentManifest.streamElements[i]; + if (streamElement.chunkCount > 0) { + long elementLiveEdgeTimestampUs = + streamElement.getStartTimeUs(streamElement.chunkCount - 1) + + streamElement.getChunkDurationUs(streamElement.chunkCount - 1); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs); + } + } + return liveEdgeTimestampUs - liveEdgeLatencyUs; + } + + private StreamElement getElement(SmoothStreamingManifest manifest) { + return manifest.streamElements[streamElementIndex]; + } + private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) { TrackElement trackElement = streamElement.tracks[trackIndex]; String mimeType = trackElement.mimeType; From 9cfe5fcf44337e3eeee52ba9326cfd3273aa722a Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 25 Sep 2014 20:29:44 +0100 Subject: [PATCH 027/110] API level 21 enhancements for ExoPlayer playbacks. - Use native frame release timing in video renderer for smoother video playback. - Avoid unnecessary memory copy steps in audio renderer. - Use non-blocking AudioTrack API. --- demo/build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 2 +- demo/src/main/project.properties | 2 +- library/build.gradle | 2 +- library/src/main/AndroidManifest.xml | 2 +- .../MediaCodecAudioTrackRenderer.java | 63 ++++---- .../MediaCodecVideoTrackRenderer.java | 151 +++++++++++------- library/src/main/project.properties | 2 +- 8 files changed, 133 insertions(+), 93 deletions(-) diff --git a/demo/build.gradle b/demo/build.gradle index a91c1ab2ca..c8db67dba5 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -19,7 +19,7 @@ android { defaultConfig { minSdkVersion 16 - targetSdkVersion 19 + targetSdkVersion 21 } buildTypes { release { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 8812dbc014..42075d9d8c 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ - + - + 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 8a75331465..cd4ed45292 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -563,12 +563,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onDisabled() { + super.onDisabled(); + releaseAudioTrack(); audioSessionId = 0; - try { - releaseAudioTrack(); - } finally { - super.onDisabled(); - } } @Override @@ -620,42 +617,52 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } } - // Copy {@code buffer} into {@code temporaryBuffer}. - // TODO: Bypass this copy step on versions of Android where [redacted] is implemented. - if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) { - temporaryBuffer = new byte[bufferInfo.size]; - } - buffer.position(bufferInfo.offset); - buffer.get(temporaryBuffer, 0, bufferInfo.size); - temporaryBufferOffset = 0; temporaryBufferSize = bufferInfo.size; + buffer.position(bufferInfo.offset); + if (Util.SDK_INT < 21) { + // Copy {@code buffer} into {@code temporaryBuffer}. + if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) { + temporaryBuffer = new byte[bufferInfo.size]; + } + buffer.get(temporaryBuffer, 0, bufferInfo.size); + temporaryBufferOffset = 0; + } } if (audioTrack == null) { initAudioTrack(); } - // TODO: Don't bother doing this once [redacted] is fixed. - // Work out how many bytes we can write without the risk of blocking. - int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize); - int bytesToWrite = bufferSize - bytesPending; - - if (bytesToWrite > 0) { - bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite); - audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - temporaryBufferOffset += bytesToWrite; - temporaryBufferSize -= bytesToWrite; - submittedBytes += bytesToWrite; - if (temporaryBufferSize == 0) { - codec.releaseOutputBuffer(bufferIndex, false); - codecCounters.renderedOutputBufferCount++; - return true; + int bytesWritten = 0; + if (Util.SDK_INT < 21) { + // Work out how many bytes we can write without the risk of blocking. + int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize); + int bytesToWrite = bufferSize - bytesPending; + if (bytesToWrite > 0) { + bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite); + bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); + temporaryBufferOffset += bytesWritten; } + } else { + bytesWritten = writeNonBlockingV21(audioTrack, buffer, temporaryBufferSize); + } + + temporaryBufferSize -= bytesWritten; + submittedBytes += bytesWritten; + if (temporaryBufferSize == 0) { + codec.releaseOutputBuffer(bufferIndex, false); + codecCounters.renderedOutputBufferCount++; + return true; } return false; } + @TargetApi(21) + private int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, AudioTrack.WRITE_NON_BLOCKING); + } + /** * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as * an unsigned 32 bit integer, which also wraps around periodically. This method returns the 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 565ab41723..3c9473585c 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.TraceUtil; +import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaCodec; @@ -93,7 +94,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { private final int maxDroppedFrameCountToNotify; private Surface surface; - private boolean drawnToSurface; + private boolean reportedDrawnToSurface; private boolean renderedFirstFrame; private long joiningDeadlineUs; private long droppedFrameAccumulationStartTimeMs; @@ -270,7 +271,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onStopped() { joiningDeadlineUs = -1; - notifyAndResetDroppedFrameCount(); + maybeNotifyDroppedFrameCount(); super.onStopped(); } @@ -303,7 +304,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { return; } this.surface = surface; - this.drawnToSurface = false; + this.reportedDrawnToSurface = false; int state = getState(); if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) { releaseCodec(); @@ -369,24 +370,37 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } if (!renderedFirstFrame) { - renderOutputBuffer(codec, bufferIndex); + renderOutputBufferImmediate(codec, bufferIndex); renderedFirstFrame = true; return true; } if (getState() == TrackRenderer.STATE_STARTED && earlyUs < 30000) { - if (earlyUs > 11000) { - // We're a little too early to render the frame. Sleep until the frame can be rendered. - // Note: The 11ms threshold was chosen fairly arbitrarily. - try { - // Subtracting 10000 rather than 11000 ensures that the sleep time will be at least 1ms. - Thread.sleep((earlyUs - 10000) / 1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + if (Util.SDK_INT >= 21) { + // Let the underlying framework time the release. + if (earlyUs < 50000) { + renderOutputBufferTimedV21(codec, bufferIndex, System.nanoTime() + (earlyUs * 1000L)); + return true; } + return false; + } else { + // We need to time the release ourselves. + if (earlyUs < 30000) { + if (earlyUs > 11000) { + // We're a little too early to render the frame. Sleep until the frame can be rendered. + // Note: The 11ms threshold was chosen fairly arbitrarily. + try { + // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. + Thread.sleep((earlyUs - 10000) / 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + renderOutputBufferImmediate(codec, bufferIndex); + return true; + } + return false; } - renderOutputBuffer(codec, bufferIndex); - return true; } // We're either not playing, or it's not time to render the frame yet. @@ -407,65 +421,84 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { codecCounters.droppedOutputBufferCount++; droppedFrameCount++; if (droppedFrameCount == maxDroppedFrameCountToNotify) { - notifyAndResetDroppedFrameCount(); + maybeNotifyDroppedFrameCount(); } } - private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { - if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight - || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { - lastReportedWidth = currentWidth; - lastReportedHeight = currentHeight; - lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; - notifyVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); - } - TraceUtil.beginSection("renderVideoBuffer"); + private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("renderVideoBufferImmediate"); codec.releaseOutputBuffer(bufferIndex, true); TraceUtil.endSection(); codecCounters.renderedOutputBufferCount++; - if (!drawnToSurface) { - drawnToSurface = true; - notifyDrawnToSurface(surface); - } + maybeNotifyDrawnToSurface(); } - private void notifyVideoSizeChanged(final int width, final int height, - final float pixelWidthHeightRatio) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); - } - }); - } + @TargetApi(21) + private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long nanoTime) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("releaseOutputBufferTimed"); + codec.releaseOutputBuffer(bufferIndex, nanoTime); + TraceUtil.endSection(); + codecCounters.renderedOutputBufferCount++; + maybeNotifyDrawnToSurface(); } - private void notifyDrawnToSurface(final Surface surface) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrawnToSurface(surface); - } - }); + private void maybeNotifyVideoSizeChanged() { + if (eventHandler == null || eventListener == null + || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight + && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) { + return; } + // Make final copies to ensure the runnable reports the correct values. + final int currentWidth = this.currentWidth; + final int currentHeight = this.currentHeight; + final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); + } + }); + // Update the last reported values. + lastReportedWidth = currentWidth; + lastReportedHeight = currentHeight; + lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; } - private void notifyAndResetDroppedFrameCount() { - if (eventHandler != null && eventListener != null && droppedFrameCount > 0) { - long now = SystemClock.elapsedRealtime(); - final int countToNotify = droppedFrameCount; - final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; - droppedFrameCount = 0; - droppedFrameAccumulationStartTimeMs = now; - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDroppedFrames(countToNotify, elapsedToNotify); - } - }); + private void maybeNotifyDrawnToSurface() { + if (eventHandler == null || eventListener == null || reportedDrawnToSurface) { + return; } + // Make a final copy to ensure the runnable reports the correct surface. + final Surface surface = this.surface; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrawnToSurface(surface); + } + }); + // Record that we have reported that the surface has been drawn to. + reportedDrawnToSurface = true; + } + + private void maybeNotifyDroppedFrameCount() { + if (eventHandler == null || eventListener == null || droppedFrameCount == 0) { + return; + } + long now = SystemClock.elapsedRealtime(); + // Make final copies to ensure the runnable reports the correct values. + final int countToNotify = droppedFrameCount; + final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDroppedFrames(countToNotify, elapsedToNotify); + } + }); + // Reset the dropped frame tracking. + droppedFrameCount = 0; + droppedFrameAccumulationStartTimeMs = now; } } diff --git a/library/src/main/project.properties b/library/src/main/project.properties index 8e4bc5fdce..b756f4487f 100644 --- a/library/src/main/project.properties +++ b/library/src/main/project.properties @@ -8,5 +8,5 @@ # project structure. # Project target. -target=android-19 +target=android-21 android.library=true From e99aaa4d67606183026228da78d258c45e25d8ec Mon Sep 17 00:00:00 2001 From: ojw28 Date: Mon, 29 Sep 2014 16:59:08 +0100 Subject: [PATCH 028/110] Update CaptionStyleCompat for L. --- .../exoplayer/text/CaptionStyleCompat.java | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java index 60cc8ab129..3e406b5854 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java +++ b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java @@ -104,14 +104,19 @@ public final class CaptionStyleCompat { /** * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. * - * @param style A {@link CaptionStyle}. + * @param captionStyle A {@link CaptionStyle}. * @return The equivalent {@link CaptionStyleCompat}. */ @TargetApi(19) - public static CaptionStyleCompat createFromCaptionStyle(CaptionStyle style) { - int windowColor = Util.SDK_INT >= 21 ? getWindowColorV21(style) : Color.TRANSPARENT; - return new CaptionStyleCompat(style.foregroundColor, style.backgroundColor, windowColor, - style.edgeType, style.edgeColor, style.getTypeface()); + public static CaptionStyleCompat createFromCaptionStyle( + CaptioningManager.CaptionStyle captionStyle) { + if (Util.SDK_INT >= 21) { + return createFromCaptionStyleV21(captionStyle); + } else { + // Note - Any caller must be on at least API level 19 of greater (because CaptionStyle did + // not exist in earlier API levels). + return createFromCaptionStyleV19(captionStyle); + } } /** @@ -132,11 +137,24 @@ public final class CaptionStyleCompat { this.typeface = typeface; } - @SuppressWarnings("unused") + @TargetApi(19) + private static CaptionStyleCompat createFromCaptionStyleV19( + CaptioningManager.CaptionStyle captionStyle) { + return new CaptionStyleCompat( + captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT, + captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface()); + } + @TargetApi(21) - private static int getWindowColorV21(CaptioningManager.CaptionStyle captionStyle) { - // TODO: Uncomment when building against API level 21. - return Color.TRANSPARENT; //captionStyle.windowColor; + private static CaptionStyleCompat createFromCaptionStyleV21( + CaptioningManager.CaptionStyle captionStyle) { + return new CaptionStyleCompat( + captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor, + captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor, + captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor, + captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType, + captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor, + captionStyle.getTypeface()); } } From 8ea3f9805c495a0deef051aca2909b3533158c34 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 1 Oct 2014 21:23:50 +0100 Subject: [PATCH 029/110] Add class to enable loading of out-of-band subtitle files. --- .../chunk/SingleSampleChunkSource.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java new file mode 100644 index 0000000000..ffb90eaefd --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java @@ -0,0 +1,108 @@ +/* + * 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.chunk; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TrackInfo; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; + +import java.io.IOException; +import java.util.List; + +/** + * A chunk source that provides a single chunk containing a single sample. + *

+ * An example use case for this implementation is to act as the source for loading out-of-band + * subtitles, where subtitles for the entire video are delivered as a single file. + */ +public class SingleSampleChunkSource implements ChunkSource { + + private final DataSource dataSource; + private final DataSpec dataSpec; + private final Format format; + private final long durationUs; + private final MediaFormat mediaFormat; + private final TrackInfo trackInfo; + + /** + * @param dataSource A {@link DataSource} suitable for loading the sample data. + * @param dataSpec Defines the location of the sample. + * @param format The format of the sample. + * @param durationUs The duration of the sample in microseconds. + * @param mediaFormat The sample media format. May be null. + */ + public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format, + long durationUs, MediaFormat mediaFormat) { + this.dataSource = dataSource; + this.dataSpec = dataSpec; + this.format = format; + this.durationUs = durationUs; + this.mediaFormat = mediaFormat; + trackInfo = new TrackInfo(format.mimeType, durationUs); + } + + @Override + public TrackInfo getTrackInfo() { + return trackInfo; + } + + @Override + public void getMaxVideoDimensions(MediaFormat out) { + // Do nothing. + } + + @Override + public void enable() { + // Do nothing. + } + + @Override + public void continueBuffering(long playbackPositionUs) { + // Do nothing. + } + + @Override + public void getChunkOperation(List queue, long seekPositionUs, + long playbackPositionUs, ChunkOperationHolder out) { + if (!queue.isEmpty()) { + // We've already provided the single sample. + return; + } + out.chunk = initChunk(); + } + + @Override + public void disable(List queue) { + // Do nothing. + } + + @Override + public IOException getError() { + return null; + } + + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. + } + + private SingleSampleMediaChunk initChunk() { + return new SingleSampleMediaChunk(dataSource, dataSpec, format, 0, 0, durationUs, -1, + mediaFormat); + } + +} From 9fc963acc647b9cec4e9b0064c32bd8e6459b915 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 1 Oct 2014 21:25:02 +0100 Subject: [PATCH 030/110] Add missing param documentation. --- .../java/com/google/android/exoplayer/chunk/ChunkSource.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java index 67330957ba..cc3a1f9d6c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java @@ -45,6 +45,8 @@ public interface ChunkSource { * the supplied {@link MediaFormat}. Other implementations do nothing. *

* Only called when the source is enabled. + * + * @param out The {@link MediaFormat} on which the maximum video dimensions should be set. */ void getMaxVideoDimensions(MediaFormat out); From ea1ab674a402e446e3d024099897ab4dd9d8c39d Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 1 Oct 2014 21:26:12 +0100 Subject: [PATCH 031/110] Strip trailing newline from WebVTT subtitles. --- .../exoplayer/text/webvtt/WebvttSubtitle.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java index 0155636033..cc6bdc4ef4 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java @@ -79,15 +79,21 @@ public class WebvttSubtitle implements Subtitle { @Override public String getText(long timeUs) { - StringBuilder subtitleStringBuilder = new StringBuilder(); + StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < cueTimesUs.length; i += 2) { if ((cueTimesUs[i] <= timeUs) && (timeUs < cueTimesUs[i + 1])) { - subtitleStringBuilder.append(cueText[i / 2]); + stringBuilder.append(cueText[i / 2]); } } - return (subtitleStringBuilder.length() > 0) ? subtitleStringBuilder.toString() : null; + int stringLength = stringBuilder.length(); + if (stringLength > 0 && stringBuilder.charAt(stringLength - 1) == '\n') { + // Adjust the length to remove the trailing newline character. + stringLength -= 1; + } + + return stringLength == 0 ? null : stringBuilder.substring(0, stringLength); } } From 8c665e3dd2d0c6dfb9a29e29f8885a9658461455 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 1 Oct 2014 21:27:25 +0100 Subject: [PATCH 032/110] Improve subtitle handling. - Move parsing onto a background thread. This is analogous to how frame decoding is pushed to MediaCodec, and should prevent possible jank when new subtitle samples are parsed. This is more important for out-of-band subtitles, which can take a second or two to parse fully. - Add Useful DataSpec method. --- .../exoplayer/text/TextTrackRenderer.java | 63 ++++++++++--------- .../android/exoplayer/upstream/DataSpec.java | 9 +++ 2 files changed, 43 insertions(+), 29 deletions(-) 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 4765a8e8b4..405b778209 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 @@ -20,20 +20,18 @@ import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.VerboseLogUtil; import android.annotation.TargetApi; import android.os.Handler; import android.os.Handler.Callback; +import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.util.Log; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; /** * A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a @@ -63,7 +61,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { private final Handler textRendererHandler; private final TextRenderer textRenderer; private final SampleSource source; - private final SampleHolder sampleHolder; private final MediaFormatHolder formatHolder; private final SubtitleParser subtitleParser; @@ -73,6 +70,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { private boolean inputStreamEnded; private Subtitle subtitle; + private SubtitleParserHelper parserHelper; + private HandlerThread parserThread; private int nextSubtitleEventIndex; private boolean textRendererNeedsUpdate; @@ -94,7 +93,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); formatHolder = new MediaFormatHolder(); - sampleHolder = new SampleHolder(true); } @Override @@ -119,6 +117,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { @Override protected void onEnabled(long timeUs, boolean joining) { source.enable(trackIndex, timeUs); + parserThread = new HandlerThread("textParser"); + parserThread.start(); + parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser); seekToInternal(timeUs); } @@ -136,7 +137,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { || subtitle.getLastEventTime() <= timeUs)) { subtitle = null; } - resetSampleData(); + parserHelper.flush(); clearTextRenderer(); syncNextEventIndex(timeUs); textRendererNeedsUpdate = subtitle != null; @@ -152,9 +153,27 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { currentPositionUs = timeUs; - // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance - // to the next event. - if (subtitle != null) { + if (parserHelper.isParsing()) { + return; + } + + Subtitle dequeuedSubtitle = null; + if (subtitle == null) { + try { + dequeuedSubtitle = parserHelper.getAndClearResult(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + if (subtitle == null && dequeuedSubtitle != null) { + // We've dequeued a new subtitle. Sync the event index and update the subtitle. + subtitle = dequeuedSubtitle; + syncNextEventIndex(timeUs); + textRendererNeedsUpdate = true; + } else if (subtitle != null) { + // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we + // advance to the next event. long nextEventTimeUs = getNextEventTime(); while (nextEventTimeUs <= timeUs) { nextSubtitleEventIndex++; @@ -170,26 +189,16 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { // We don't have a subtitle. Try and read the next one from the source, and if we succeed then // sync and set textRendererNeedsUpdate. if (subtitle == null) { - boolean resetSampleHolder = false; try { + SampleHolder sampleHolder = parserHelper.getSampleHolder(); int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { - resetSampleHolder = true; - InputStream subtitleInputStream = - new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size); - subtitle = subtitleParser.parse(subtitleInputStream, null, sampleHolder.timeUs); - syncNextEventIndex(timeUs); - textRendererNeedsUpdate = true; + parserHelper.startParseOperation(); } else if (result == SampleSource.END_OF_STREAM) { inputStreamEnded = true; } } catch (IOException e) { - resetSampleHolder = true; throw new ExoPlaybackException(e); - } finally { - if (resetSampleHolder) { - resetSampleData(); - } } } @@ -208,7 +217,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { protected void onDisabled() { source.disable(trackIndex); subtitle = null; - resetSampleData(); + parserThread.quit(); + parserThread = null; + parserHelper = null; clearTextRenderer(); } @@ -255,12 +266,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { : (subtitle.getEventTime(nextSubtitleEventIndex)); } - private void resetSampleData() { - if (sampleHolder.data != null) { - sampleHolder.data.position(0); - } - } - private void updateTextRenderer(long timeUs) { String text = subtitle.getText(timeUs); log("updateTextRenderer; text=: " + text); @@ -296,7 +301,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { private void log(String logMessage) { if (VerboseLogUtil.isTagEnabled(TAG)) { - Log.v(TAG, "type=" + AdaptationSet.TYPE_TEXT + ", " + logMessage); + Log.v(TAG, logMessage); } } 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 41f758be8a..ff3b7dda0d 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 @@ -53,6 +53,15 @@ public final class DataSpec { */ public final String key; + /** + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + */ + public DataSpec(Uri uri) { + this(uri, 0, C.LENGTH_UNBOUNDED, null); + } + /** * Construct a {@link DataSpec} for which {@link #uriIsFullStream} is true. * From dec40bcbd36e84a35367160a008818a9402ac04f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 1 Oct 2014 22:14:44 +0100 Subject: [PATCH 033/110] Add file missing from "Improve subtitle handling" change. --- .../exoplayer/text/SubtitleParserHelper.java | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java new file mode 100644 index 0000000000..cafdb89f25 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java @@ -0,0 +1,147 @@ +/* + * 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.text; + +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.util.Assertions; + +import android.media.MediaCodec; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Wraps a {@link SubtitleParser}, exposing an interface similar to {@link MediaCodec} for + * asynchronous parsing of subtitles. + */ +public class SubtitleParserHelper implements Handler.Callback { + + private final SubtitleParser parser; + + private final Handler handler; + private SampleHolder sampleHolder; + private boolean parsing; + private Subtitle result; + private IOException error; + + /** + * @param looper The {@link Looper} associated with the thread on which parsing should occur. + * @param parser The parser that should be used to parse the raw data. + */ + public SubtitleParserHelper(Looper looper, SubtitleParser parser) { + this.handler = new Handler(looper, this); + this.parser = parser; + flush(); + } + + /** + * Flushes the helper, canceling the current parsing operation, if there is one. + */ + public synchronized void flush() { + sampleHolder = new SampleHolder(true); + parsing = false; + result = null; + error = null; + } + + /** + * Whether the helper is currently performing a parsing operation. + * + * @return True if the helper is currently performing a parsing operation. False otherwise. + */ + public synchronized boolean isParsing() { + return parsing; + } + + /** + * Gets the holder that should be populated with data to be parsed. + *

+ * The returned holder will remain valid unless {@link #flush()} is called. If {@link #flush()} + * is called the holder is replaced, and this method should be called again to obtain the new + * holder. + * + * @return The holder that should be populated with data to be parsed. + */ + public synchronized SampleHolder getSampleHolder() { + return sampleHolder; + } + + /** + * Start a parsing operation. + *

+ * The holder returned by {@link #getSampleHolder()} should be populated with the data to be + * parsed prior to calling this method. + */ + public synchronized void startParseOperation() { + Assertions.checkState(!parsing); + parsing = true; + result = null; + error = null; + handler.obtainMessage(0, sampleHolder).sendToTarget(); + } + + /** + * Gets the result of the most recent parsing operation. + *

+ * The result is cleared as a result of calling this method, and so subsequent calls will return + * null until a subsequent parsing operation has finished. + * + * @return The result of the parsing operation, or null. + * @throws IOException If the parsing operation failed. + */ + public synchronized Subtitle getAndClearResult() throws IOException { + try { + if (error != null) { + throw error; + } + return result; + } finally { + error = null; + result = null; + } + } + + @Override + public boolean handleMessage(Message msg) { + Subtitle result; + IOException error; + SampleHolder holder = (SampleHolder) msg.obj; + try { + InputStream inputStream = new ByteArrayInputStream(holder.data.array(), 0, holder.size); + result = parser.parse(inputStream, null, sampleHolder.timeUs); + error = null; + } catch (IOException e) { + result = null; + error = e; + } + synchronized (this) { + if (sampleHolder != holder) { + // A flush has occurred since this holder was posted. Do nothing. + } else { + holder.data.position(0); + this.result = result; + this.error = error; + this.parsing = false; + } + } + return true; + } + +} From be721943c614ea04a9bbdc316ac3f2d5097b5ab5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 2 Oct 2014 12:23:08 +0100 Subject: [PATCH 034/110] Fix incorrect condition. --- .../MediaCodecVideoTrackRenderer.java | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) 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 3c9473585c..acfe6d6de2 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -375,31 +375,31 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { return true; } - if (getState() == TrackRenderer.STATE_STARTED && earlyUs < 30000) { - if (Util.SDK_INT >= 21) { - // Let the underlying framework time the release. - if (earlyUs < 50000) { - renderOutputBufferTimedV21(codec, bufferIndex, System.nanoTime() + (earlyUs * 1000L)); - return true; - } - return false; - } else { - // We need to time the release ourselves. - if (earlyUs < 30000) { - if (earlyUs > 11000) { - // We're a little too early to render the frame. Sleep until the frame can be rendered. - // Note: The 11ms threshold was chosen fairly arbitrarily. - try { - // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. - Thread.sleep((earlyUs - 10000) / 1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + if (getState() != TrackRenderer.STATE_STARTED) { + return false; + } + + if (Util.SDK_INT >= 21) { + // Let the underlying framework time the release. + if (earlyUs < 50000) { + renderOutputBufferTimedV21(codec, bufferIndex, System.nanoTime() + (earlyUs * 1000L)); + return true; + } + } else { + // We need to time the release ourselves. + if (earlyUs < 30000) { + if (earlyUs > 11000) { + // We're a little too early to render the frame. Sleep until the frame can be rendered. + // Note: The 11ms threshold was chosen fairly arbitrarily. + try { + // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. + Thread.sleep((earlyUs - 10000) / 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - renderOutputBufferImmediate(codec, bufferIndex); - return true; } - return false; + renderOutputBufferImmediate(codec, bufferIndex); + return true; } } From ac18ac087bf9d178580112edf386fb56016da786 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:13:14 +0100 Subject: [PATCH 035/110] Fix missing ->IN_SYNC transition. --- .../google/android/exoplayer/MediaCodecAudioTrackRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 8a75331465..f0613bc82c 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -616,6 +616,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { // time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to // allow time to jump backwards if it really wants to. audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); + audioTrackStartMediaTimeState = START_IN_SYNC; lastReportedCurrentPositionUs = Long.MIN_VALUE; } } From 43712ce41c659181bf8f7f8b3d133732a5ace9be Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:16:13 +0100 Subject: [PATCH 036/110] Cap AudioTrack latencies at 10 seconds and log a warning if too large. --- .../MediaCodecAudioTrackRenderer.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 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 f0613bc82c..b59744d893 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -94,10 +94,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { /** * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more - * than this amount. This is a fail safe that should not be required on correctly functioning - * devices. + * than this amount. + *

+ * This is a fail safe that should not be required on correctly functioning devices. */ - private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + *

+ * This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TRACK_LATENCY_US = 10 * MICROS_PER_SECOND; private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; @@ -515,7 +523,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { if (audioTimestampUs < audioTrackResumeSystemTimeUs) { // The timestamp corresponds to a time before the track was most recently resumed. audioTimestampSet = false; - } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) { + } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { // The timestamp time base is probably wrong. audioTimestampSet = false; Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", " @@ -531,6 +539,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { framesToDurationUs(bufferSize / frameSize); // Sanity check that the latency is non-negative. audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0); + // Sanity check that the latency isn't too large. + if (audioTrackLatencyUs > MAX_AUDIO_TRACK_LATENCY_US) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + audioTrackLatencyUs); + audioTrackLatencyUs = 0; + } } catch (Exception e) { // The method existed, but doesn't work. Don't try again. audioTrackGetLatencyMethod = null; From d4e824634c85f4f2b6b0bfac200a7bbfeddd3aba Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:17:43 +0100 Subject: [PATCH 037/110] Throw a checked exception rather than unchecked one. So that we actually catch it, rather than having the process crash! --- .../google/android/exoplayer/text/ttml/TtmlParser.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index 2fd1850f53..9c60db6b54 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.text.ttml; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.SubtitleParser; import com.google.android.exoplayer.util.MimeTypes; @@ -135,7 +136,7 @@ public class TtmlParser implements SubtitleParser { return MimeTypes.APPLICATION_TTML.equals(mimeType); } - private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) { + private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException { long duration = 0; long startTime = TtmlNode.UNDEFINED_TIME; long endTime = TtmlNode.UNDEFINED_TIME; @@ -209,10 +210,10 @@ public class TtmlParser implements SubtitleParser { * @param subframeRate The sub-framerate of the stream * @param tickRate The tick rate of the stream. * @return The parsed timestamp in microseconds. - * @throws NumberFormatException If the given string does not contain a valid time expression. + * @throws ParserException If the given string does not contain a valid time expression. */ private static long parseTimeExpression(String time, int frameRate, int subframeRate, - int tickRate) { + int tickRate) throws ParserException { Matcher matcher = CLOCK_TIME.matcher(time); if (matcher.matches()) { String hours = matcher.group(1); @@ -250,7 +251,7 @@ public class TtmlParser implements SubtitleParser { } return (long) (offsetSeconds * 1000000); } - throw new NumberFormatException("Malformed time expression: " + time); + throw new ParserException("Malformed time expression: " + time); } } From 3b4409ae0b1627d2dd238d15e2723c029b1ee181 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:19:14 +0100 Subject: [PATCH 038/110] Allow relaxation of TTML validity requirement when parsing subtitles. --- .../android/exoplayer/ParserException.java | 6 ++- .../exoplayer/text/ttml/TtmlParser.java | 47 ++++++++++++++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/ParserException.java b/library/src/main/java/com/google/android/exoplayer/ParserException.java index f3830bcba7..ce47f8aa16 100644 --- a/library/src/main/java/com/google/android/exoplayer/ParserException.java +++ b/library/src/main/java/com/google/android/exoplayer/ParserException.java @@ -26,8 +26,12 @@ public class ParserException extends IOException { super(message); } - public ParserException(Exception cause) { + public ParserException(Throwable cause) { super(cause); } + public ParserException(String message, Throwable cause) { + super(message, cause); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index 9c60db6b54..82b41d3d8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -73,8 +73,23 @@ public class TtmlParser implements SubtitleParser { private static final int DEFAULT_TICKRATE = 1; private final XmlPullParserFactory xmlParserFactory; + private final boolean strictParsing; + /** + * Equivalent to {@code TtmlParser(true)}. + */ public TtmlParser() { + this(true); + } + + /** + * @param strictParsing If true, {@link #parse(InputStream, String, long)} will throw a + * {@link ParserException} if the stream contains invalid ttml. If false, the parser will + * make a best effort to ignore minor errors in the stream. Note however that a + * {@link ParserException} will still be thrown when this is not possible. + */ + public TtmlParser(boolean strictParsing) { + this.strictParsing = strictParsing; try { xmlParserFactory = XmlPullParserFactory.newInstance(); } catch (XmlPullParserException e) { @@ -90,21 +105,31 @@ public class TtmlParser implements SubtitleParser { xmlParser.setInput(inputStream, inputEncoding); TtmlSubtitle ttmlSubtitle = null; LinkedList nodeStack = new LinkedList(); - int unsupportedTagDepth = 0; + int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { TtmlNode parent = nodeStack.peekLast(); - if (unsupportedTagDepth == 0) { + if (unsupportedNodeDepth == 0) { String name = xmlParser.getName(); if (eventType == XmlPullParser.START_TAG) { if (!isSupportedTag(name)) { - Log.w(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); - unsupportedTagDepth++; + Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); + unsupportedNodeDepth++; } else { - TtmlNode node = parseNode(xmlParser, parent); - nodeStack.addLast(node); - if (parent != null) { - parent.addChild(node); + try { + TtmlNode node = parseNode(xmlParser, parent); + nodeStack.addLast(node); + if (parent != null) { + parent.addChild(node); + } + } catch (ParserException e) { + if (strictParsing) { + throw e; + } else { + Log.e(TAG, "Suppressing parser error", e); + // Treat the node (and by extension, all of its children) as unsupported. + unsupportedNodeDepth++; + } } } } else if (eventType == XmlPullParser.TEXT) { @@ -117,9 +142,9 @@ public class TtmlParser implements SubtitleParser { } } else { if (eventType == XmlPullParser.START_TAG) { - unsupportedTagDepth++; + unsupportedNodeDepth++; } else if (eventType == XmlPullParser.END_TAG) { - unsupportedTagDepth--; + unsupportedNodeDepth--; } } xmlParser.next(); @@ -127,7 +152,7 @@ public class TtmlParser implements SubtitleParser { } return ttmlSubtitle; } catch (XmlPullParserException xppe) { - throw new IOException("Unable to parse source", xppe); + throw new ParserException("Unable to parse source", xppe); } } From 027d9eefbdfdfbc74208376cc1834e54a8cd1d93 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:26:01 +0100 Subject: [PATCH 039/110] Smoother playback #1. Propagate elapsedRealtimeUs to the video renderer. This allows the renderer to calculate and adjust for the elapsed time since the start of the current rendering loop. Typically this is <2ms, but there situations where it can go higher (normally when the video renderer ends up processing more than 1 output buffer in a single loop). Also made variable naming more consistent throughout the package. --- .../demo/full/player/DebugTrackRenderer.java | 6 +- .../android/exoplayer/DummyTrackRenderer.java | 4 +- .../exoplayer/ExoPlayerImplInternal.java | 10 +-- .../exoplayer/FrameworkSampleSource.java | 20 +++--- .../google/android/exoplayer/MediaClock.java | 24 +++---- .../MediaCodecAudioTrackRenderer.java | 16 ++--- .../exoplayer/MediaCodecTrackRenderer.java | 39 +++++----- .../MediaCodecVideoTrackRenderer.java | 15 ++-- .../android/exoplayer/SampleSource.java | 18 ++--- .../android/exoplayer/TrackRenderer.java | 42 ++++++----- .../exoplayer/chunk/ChunkSampleSource.java | 72 +++++++++---------- .../exoplayer/text/TextTrackRenderer.java | 46 ++++++------ 12 files changed, 161 insertions(+), 151 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 8093bad814..d848dd3908 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 @@ -68,10 +68,10 @@ import android.widget.TextView; } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { maybeFail(); - if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) { - currentPositionUs = timeUs; + if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) { + currentPositionUs = positionUs; textView.post(this); } } diff --git a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java index 4bafdd07b8..4dd5ef4a42 100644 --- a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java @@ -40,12 +40,12 @@ public class DummyTrackRenderer extends TrackRenderer { } @Override - protected void seekTo(long timeUs) { + protected void seekTo(long positionUs) { throw new IllegalStateException(); } @Override - protected void doSomeWork(long timeUs) { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) { throw new IllegalStateException(); } 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 0184ea9956..2dfac29519 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -77,6 +77,7 @@ import java.util.List; private int state; private int customMessagesSent = 0; private int customMessagesProcessed = 0; + private long elapsedRealtimeUs; private volatile long durationUs; private volatile long positionUs; @@ -383,7 +384,8 @@ import java.util.List; positionUs = timeSourceTrackRenderer != null && enabledRenderers.contains(timeSourceTrackRenderer) ? timeSourceTrackRenderer.getCurrentPositionUs() : - mediaClock.getTimeUs(); + mediaClock.getPositionUs(); + elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; } private void doSomeWork() throws ExoPlaybackException { @@ -399,7 +401,7 @@ import java.util.List; // TODO: Each renderer should return the maximum delay before which it wishes to be // invoked again. The minimum of these values should then be used as the delay before the next // invocation of this method. - renderer.doSomeWork(positionUs); + renderer.doSomeWork(positionUs, elapsedRealtimeUs); isEnded = isEnded && renderer.isEnded(); allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); @@ -462,7 +464,7 @@ import java.util.List; rebuffering = false; positionUs = positionMs * 1000L; mediaClock.stop(); - mediaClock.setTimeUs(positionUs); + mediaClock.setPositionUs(positionUs); if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) { return; } @@ -582,7 +584,7 @@ import java.util.List; if (renderer == timeSourceTrackRenderer) { // We've been using timeSourceTrackRenderer to advance the current position, but it's // being disabled. Sync mediaClock so that it can take over timing responsibilities. - mediaClock.setTimeUs(renderer.getCurrentPositionUs()); + mediaClock.setPositionUs(renderer.getCurrentPositionUs()); } ensureStopped(renderer); enabledRenderers.remove(renderer); 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 63afbdf0f5..0fc39b0e1a 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -50,7 +50,7 @@ public final class FrameworkSampleSource implements SampleSource { private int[] trackStates; private boolean[] pendingDiscontinuities; - private long seekTimeUs; + private long seekPositionUs; public FrameworkSampleSource(Context context, Uri uri, Map headers, int downstreamRendererCount) { @@ -94,16 +94,16 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public void enable(int track, long timeUs) { + public void enable(int track, long positionUs) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); trackStates[track] = TRACK_STATE_ENABLED; extractor.selectTrack(track); - seekToUs(timeUs); + seekToUs(positionUs); } @Override - public boolean continueBuffering(long playbackPositionUs) { + public boolean continueBuffering(long positionUs) { // 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 @@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); @@ -144,7 +144,7 @@ public final class FrameworkSampleSource implements SampleSource { if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) { sampleHolder.cryptoInfo.setFromExtractorV16(extractor); } - seekTimeUs = -1; + seekPositionUs = -1; extractor.advance(); return SAMPLE_READ; } else { @@ -168,13 +168,13 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public void seekToUs(long timeUs) { + public void seekToUs(long positionUs) { Assertions.checkState(prepared); - if (seekTimeUs != timeUs) { + if (seekPositionUs != positionUs) { // Avoid duplicate calls to the underlying extractor's seek method in the case that there // have been no interleaving calls to advance. - seekTimeUs = timeUs; - extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + seekPositionUs = positionUs; + extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); for (int i = 0; i < trackStates.length; ++i) { if (trackStates[i] != TRACK_STATE_DISABLED) { pendingDiscontinuities[i] = true; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaClock.java b/library/src/main/java/com/google/android/exoplayer/MediaClock.java index 9abd3c1f03..c2696e3b74 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaClock.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaClock.java @@ -29,10 +29,10 @@ import android.os.SystemClock; /** * The media time when the clock was last set or stopped. */ - private long timeUs; + private long positionUs; /** - * The difference between {@link SystemClock#elapsedRealtime()} and {@link #timeUs} + * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs} * when the clock was last set or started. */ private long deltaUs; @@ -43,7 +43,7 @@ import android.os.SystemClock; public void start() { if (!started) { started = true; - deltaUs = elapsedRealtimeMinus(timeUs); + deltaUs = elapsedRealtimeMinus(positionUs); } } @@ -52,28 +52,28 @@ import android.os.SystemClock; */ public void stop() { if (started) { - timeUs = elapsedRealtimeMinus(deltaUs); + positionUs = elapsedRealtimeMinus(deltaUs); started = false; } } /** - * @param timeUs The time to set in microseconds. + * @param timeUs The position to set in microseconds. */ - public void setTimeUs(long timeUs) { - this.timeUs = timeUs; + public void setPositionUs(long timeUs) { + this.positionUs = timeUs; deltaUs = elapsedRealtimeMinus(timeUs); } /** - * @return The current time in microseconds. + * @return The current position in microseconds. */ - public long getTimeUs() { - return started ? elapsedRealtimeMinus(deltaUs) : timeUs; + public long getPositionUs() { + return started ? elapsedRealtimeMinus(deltaUs) : positionUs; } - private long elapsedRealtimeMinus(long microSeconds) { - return SystemClock.elapsedRealtime() * 1000 - microSeconds; + private long elapsedRealtimeMinus(long toSubtractUs) { + return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; } } 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 b59744d893..5027cb7830 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -269,14 +269,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onEnabled(long timeUs, boolean joining) { - super.onEnabled(timeUs, joining); + protected void onEnabled(long positionUs, boolean joining) { + super.onEnabled(positionUs, joining); lastReportedCurrentPositionUs = Long.MIN_VALUE; } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { - super.doSomeWork(timeUs); + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + super.doSomeWork(positionUs, elapsedRealtimeUs); maybeSampleSyncParams(); } @@ -585,16 +585,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void seekTo(long timeUs) throws ExoPlaybackException { - super.seekTo(timeUs); + protected void seekTo(long positionUs) throws ExoPlaybackException { + super.seekTo(positionUs); // TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed. releaseAudioTrack(); lastReportedCurrentPositionUs = Long.MIN_VALUE; } @Override - protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) throws ExoPlaybackException { if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); 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 4124a22c27..5e28f36d1b 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -217,13 +217,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected void onEnabled(long timeUs, boolean joining) { - source.enable(trackIndex, timeUs); + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; waitingForKeys = false; - currentPositionUs = timeUs; + currentPositionUs = positionUs; } /** @@ -367,9 +367,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected void seekTo(long timeUs) throws ExoPlaybackException { - currentPositionUs = timeUs; - source.seekToUs(timeUs); + protected void seekTo(long positionUs) throws ExoPlaybackException { + currentPositionUs = positionUs; + source.seekToUs(positionUs); sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; @@ -387,22 +387,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { try { - sourceState = source.continueBuffering(timeUs) + sourceState = source.continueBuffering(positionUs) ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) : SOURCE_STATE_NOT_READY; checkForDiscontinuity(); if (format == null) { readFormat(); } else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) { - discardSamples(timeUs); + discardSamples(positionUs); } else { if (codec == null && shouldInitCodec()) { maybeInitCodec(); } if (codec != null) { - while (drainOutputBuffer(timeUs)) {} + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} if (feedInputBuffer(true)) { while (feedInputBuffer(false)) {} } @@ -421,10 +421,10 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } } - private void discardSamples(long timeUs) throws IOException, ExoPlaybackException { + private void discardSamples(long positionUs) throws IOException, ExoPlaybackException { sampleHolder.data = null; int result = SampleSource.SAMPLE_READ; - while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) { + while (result == SampleSource.SAMPLE_READ && currentPositionUs <= positionUs) { result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { if (!sampleHolder.decodeOnly) { @@ -469,7 +469,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { /** * @param firstFeed True if this is the first call to this method from the current invocation of - * {@link #doSomeWork(long)}. False otherwise. + * {@link #doSomeWork(long, long)}. False otherwise. * @return True if it may be possible to feed more input data. False otherwise. * @throws IOException If an error occurs reading data from the upstream source. * @throws ExoPlaybackException If an error occurs feeding the input buffer. @@ -694,7 +694,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @return True if it may be possible to drain more output data. False otherwise. * @throws ExoPlaybackException If an error occurs draining the output buffer. */ - private boolean drainOutputBuffer(long timeUs) throws ExoPlaybackException { + private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { if (outputStreamEnded) { return false; } @@ -722,8 +723,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { boolean decodeOnly = decodeOnlyPresentationTimestamps.contains( outputBufferInfo.presentationTimeUs); - if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo, - outputIndex, decodeOnly)) { + if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], + outputBufferInfo, outputIndex, decodeOnly)) { if (decodeOnly) { decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs); } else { @@ -743,9 +744,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * longer required. False otherwise. * @throws ExoPlaybackException If an error occurs processing the output buffer. */ - protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) - throws ExoPlaybackException; + protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, + MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, + boolean shouldSkip) throws ExoPlaybackException; /** * Returns the name of the secure variant of a given decoder. 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 565ab41723..a19be59df1 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -225,8 +225,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onEnabled(long startTimeUs, boolean joining) { - super.onEnabled(startTimeUs, joining); + protected void onEnabled(long positionUs, boolean joining) { + super.onEnabled(positionUs, joining); renderedFirstFrame = false; if (joining && allowedJoiningTimeUs > 0) { joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs; @@ -234,8 +234,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void seekTo(long timeUs) throws ExoPlaybackException { - super.seekTo(timeUs); + protected void seekTo(long positionUs) throws ExoPlaybackException { + super.seekTo(positionUs); renderedFirstFrame = false; joiningDeadlineUs = -1; } @@ -354,14 +354,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) { + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) { if (shouldSkip) { skipOutputBuffer(codec, bufferIndex); return true; } - long earlyUs = bufferInfo.presentationTimeUs - timeUs; + long elapsedSinceStartOfLoop = SystemClock.elapsedRealtime() * 1000 - elapsedRealtimeUs; + long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoop; if (earlyUs < -30000) { // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex); 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 2f26d30e9a..9a3d40819b 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -85,9 +85,9 @@ public interface SampleSource { * This method should not be called until after the source has been successfully prepared. * * @param track The track to enable. - * @param timeUs The player's current playback position. + * @param positionUs The player's current playback position. */ - public void enable(int track, long timeUs); + public void enable(int track, long positionUs); /** * Disable the specified track. @@ -101,12 +101,12 @@ public interface SampleSource { /** * Indicates to the source that it should still be buffering data. * - * @param playbackPositionUs The current playback position. + * @param positionUs 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 boolean continueBuffering(long playbackPositionUs) throws IOException; + public boolean continueBuffering(long positionUs) throws IOException; /** * Attempts to read either a sample, a new format or or a discontinuity from the source. @@ -118,7 +118,7 @@ public interface SampleSource { * than the one for which data was requested. * * @param track The track from which to read. - * @param playbackPositionUs The current playback position. + * @param positionUs The current playback position. * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format. * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If * the caller requires the sample data then it must ensure that {@link SampleHolder#data} @@ -129,7 +129,7 @@ public interface SampleSource { * {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}. * @throws IOException If an error occurred reading from the source. */ - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException; /** @@ -137,16 +137,16 @@ public interface SampleSource { *

* This method should not be called until after the source has been successfully prepared. * - * @param timeUs The seek position in microseconds. + * @param positionUs The seek position in microseconds. */ - public void seekToUs(long timeUs); + public void seekToUs(long positionUs); /** * Returns an estimate of the position up to which data is buffered. *

* This method should not be called until after the source has been successfully prepared. * - * @return An estimate of the absolute position in micro-seconds up to which data is buffered, + * @return An estimate of the absolute position in microseconds up to which data is buffered, * 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. */ 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 f27433d06e..66e20291f7 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.util.Assertions; +import android.os.SystemClock; + /** * Renders a single component of media. * @@ -59,7 +61,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent { */ protected static final int STATE_ENABLED = 2; /** - * The renderer is started. Calls to {@link #doSomeWork(long)} should cause the media to be + * The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be * rendered. */ protected static final int STATE_STARTED = 3; @@ -83,9 +85,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * A time source renderer is a renderer that, when started, advances its own playback position. * This means that {@link #getCurrentPositionUs()} will return increasing positions independently - * to increasing values being passed to {@link #doSomeWork(long)}. A player may have at most one - * time source renderer. If provided, the player will use such a renderer as its source of time - * during playback. + * to increasing values being passed to {@link #doSomeWork(long, long)}. A player may have at most + * one time source renderer. If provided, the player will use such a renderer as its source of + * time during playback. *

* This method may be called when the renderer is in any state. * @@ -136,15 +138,15 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * Enable the renderer. * - * @param timeUs The player's current position. + * @param positionUs The player's current position. * @param joining Whether this renderer is being enabled to join an ongoing playback. If true * then {@link #start} must be called immediately after this method returns (unless a * {@link ExoPlaybackException} is thrown). */ - /* package */ final void enable(long timeUs, boolean joining) throws ExoPlaybackException { + /* package */ final void enable(long positionUs, boolean joining) throws ExoPlaybackException { Assertions.checkState(state == TrackRenderer.STATE_PREPARED); state = TrackRenderer.STATE_ENABLED; - onEnabled(timeUs, joining); + onEnabled(positionUs, joining); } /** @@ -152,18 +154,18 @@ public abstract class TrackRenderer implements ExoPlayerComponent { *

* The default implementation is a no-op. * - * @param timeUs The player's current position. + * @param positionUs The player's current position. * @param joining Whether this renderer is being enabled to join an ongoing playback. If true * then {@link #onStarted} is guaranteed to be called immediately after this method returns * (unless a {@link ExoPlaybackException} is thrown). * @throws ExoPlaybackException If an error occurs. */ - protected void onEnabled(long timeUs, boolean joining) throws ExoPlaybackException { + protected void onEnabled(long positionUs, boolean joining) throws ExoPlaybackException { // Do nothing. } /** - * Starts the renderer, meaning that calls to {@link #doSomeWork(long)} will cause the + * Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the * track to be rendered. */ /* package */ final void start() throws ExoPlaybackException { @@ -289,10 +291,14 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @param timeUs The current playback time. + * @param positionUs The current media time in microseconds, measured at the start of the + * current iteration of the rendering loop. + * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at + * the start of the current iteration of the rendering loop. * @throws ExoPlaybackException If an error occurs. */ - protected abstract void doSomeWork(long timeUs) throws ExoPlaybackException; + protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException; /** * Returns the duration of the media being rendered. @@ -300,7 +306,7 @@ 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_US} if + * @return The duration of the track in microseconds, 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_US} if the duration is not known. */ @@ -312,17 +318,17 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @return The current playback position in micro-seconds. + * @return The current playback position in microseconds. */ protected abstract long getCurrentPositionUs(); /** - * Returns an estimate of the absolute position in micro-seconds up to which data is buffered. + * Returns an estimate of the absolute position in microseconds up to which data is buffered. *

* This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @return An estimate of the absolute position in micro-seconds up to which data is buffered, + * @return An estimate of the absolute position in microseconds up to which data is buffered, * or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if * no estimate is available. */ @@ -334,10 +340,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED} * - * @param timeUs The desired time in micro-seconds. + * @param positionUs The desired playback position in microseconds. * @throws ExoPlaybackException If an error occurs. */ - protected abstract void seekTo(long timeUs) throws ExoPlaybackException; + protected abstract void seekTo(long positionUs) throws ExoPlaybackException; @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { 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 f2f0ce031a..a436077d5f 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 @@ -154,7 +154,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private int state; private long downstreamPositionUs; private long lastSeekPositionUs; - private long pendingResetTime; + private long pendingResetPositionUs; private long lastPerformedBufferOperation; private boolean pendingDiscontinuity; @@ -219,7 +219,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public void enable(int track, long timeUs) { + public void enable(int track, long positionUs) { Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(track == 0); state = STATE_ENABLED; @@ -227,9 +227,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { loadControl.register(this, bufferSizeContribution); downstreamFormat = null; downstreamMediaFormat = null; - downstreamPositionUs = timeUs; - lastSeekPositionUs = timeUs; - restartFrom(timeUs); + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + restartFrom(positionUs); } @Override @@ -253,10 +253,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public boolean continueBuffering(long playbackPositionUs) throws IOException { + public boolean continueBuffering(long positionUs) throws IOException { Assertions.checkState(state == STATE_ENABLED); - downstreamPositionUs = playbackPositionUs; - chunkSource.continueBuffering(playbackPositionUs); + downstreamPositionUs = positionUs; + chunkSource.continueBuffering(positionUs); updateLoadControl(); if (isPendingReset() || mediaChunks.isEmpty()) { return false; @@ -271,7 +271,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(track == 0); @@ -285,7 +285,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { return NOTHING_READ; } - downstreamPositionUs = playbackPositionUs; + downstreamPositionUs = positionUs; if (isPendingReset()) { if (currentLoadableException != null) { throw currentLoadableException; @@ -304,7 +304,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { discardDownstreamMediaChunk(); mediaChunk = mediaChunks.getFirst(); mediaChunk.seekToStart(); - return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); + return readData(track, positionUs, formatHolder, sampleHolder, false); } else if (mediaChunk.isLastChunk()) { return END_OF_STREAM; } @@ -350,32 +350,32 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public void seekToUs(long timeUs) { + public void seekToUs(long positionUs) { Assertions.checkState(state == STATE_ENABLED); - downstreamPositionUs = timeUs; - lastSeekPositionUs = timeUs; - if (pendingResetTime == timeUs) { + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + if (pendingResetPositionUs == positionUs) { return; } - MediaChunk mediaChunk = getMediaChunk(timeUs); + MediaChunk mediaChunk = getMediaChunk(positionUs); if (mediaChunk == null) { - restartFrom(timeUs); + restartFrom(positionUs); pendingDiscontinuity = true; } else { - pendingDiscontinuity |= mediaChunk.seekTo(timeUs, mediaChunk == mediaChunks.getFirst()); + pendingDiscontinuity |= mediaChunk.seekTo(positionUs, mediaChunk == mediaChunks.getFirst()); discardDownstreamMediaChunks(mediaChunk); updateLoadControl(); } } - private MediaChunk getMediaChunk(long timeUs) { + private MediaChunk getMediaChunk(long positionUs) { Iterator mediaChunkIterator = mediaChunks.iterator(); while (mediaChunkIterator.hasNext()) { MediaChunk mediaChunk = mediaChunkIterator.next(); - if (timeUs < mediaChunk.startTimeUs) { + if (positionUs < mediaChunk.startTimeUs) { return null; - } else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) { + } else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) { return mediaChunk; } } @@ -386,7 +386,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { public long getBufferedPositionUs() { Assertions.checkState(state == STATE_ENABLED); if (isPendingReset()) { - return pendingResetTime; + return pendingResetPositionUs; } MediaChunk mediaChunk = mediaChunks.getLast(); Chunk currentLoadable = currentLoadableHolder.chunk; @@ -448,7 +448,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } clearCurrentLoadable(); if (state == STATE_ENABLED) { - restartFrom(pendingResetTime); + restartFrom(pendingResetPositionUs); } else { clearMediaChunks(); loadControl.trimAllocator(); @@ -476,8 +476,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { // no-op } - private void restartFrom(long timeUs) { - pendingResetTime = timeUs; + private void restartFrom(long positionUs) { + pendingResetPositionUs = positionUs; if (loader.isLoading()) { loader.cancelLoading(); } else { @@ -501,7 +501,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private void updateLoadControl() { long loadPositionUs; if (isPendingReset()) { - loadPositionUs = pendingResetTime; + loadPositionUs = pendingResetPositionUs; } else { MediaChunk lastMediaChunk = mediaChunks.getLast(); loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; @@ -529,8 +529,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { lastPerformedBufferOperation = now; currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, - currentLoadableHolder); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); } if (nextLoader) { @@ -552,8 +552,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { Chunk backedOffChunk = currentLoadableHolder.chunk; if (!isMediaChunk(backedOffChunk)) { currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, - currentLoadableHolder); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. @@ -577,7 +577,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { MediaChunk removedChunk = mediaChunks.removeLast(); Assertions.checkState(backedOffChunk == removedChunk); currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs, currentLoadableHolder); mediaChunks.add(removedChunk); @@ -603,8 +603,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { if (isMediaChunk(currentLoadable)) { MediaChunk mediaChunk = (MediaChunk) currentLoadable; if (isPendingReset()) { - mediaChunk.seekTo(pendingResetTime, false); - pendingResetTime = NO_RESET_PENDING; + mediaChunk.seekTo(pendingResetPositionUs, false); + pendingResetPositionUs = NO_RESET_PENDING; } mediaChunks.add(mediaChunk); notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false, @@ -674,7 +674,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private boolean isPendingReset() { - return pendingResetTime != NO_RESET_PENDING; + return pendingResetPositionUs != NO_RESET_PENDING; } private long getRetryDelayMillis(long errorCount) { @@ -757,13 +757,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void notifyDownstreamFormatChanged(final String formatId, final int trigger, - final long mediaTimeUs) { + final long positionUs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger, - usToMs(mediaTimeUs)); + usToMs(positionUs)); } }); } 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 405b778209..f7f38d986a 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 @@ -115,43 +115,43 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } @Override - protected void onEnabled(long timeUs, boolean joining) { - source.enable(trackIndex, timeUs); + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); parserThread = new HandlerThread("textParser"); parserThread.start(); parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser); - seekToInternal(timeUs); + seekToInternal(positionUs); } @Override - protected void seekTo(long timeUs) { - source.seekToUs(timeUs); - seekToInternal(timeUs); + protected void seekTo(long positionUs) { + source.seekToUs(positionUs); + seekToInternal(positionUs); } - private void seekToInternal(long timeUs) { + private void seekToInternal(long positionUs) { inputStreamEnded = false; - currentPositionUs = timeUs; - source.seekToUs(timeUs); - if (subtitle != null && (timeUs < subtitle.getStartTime() - || subtitle.getLastEventTime() <= timeUs)) { + currentPositionUs = positionUs; + source.seekToUs(positionUs); + if (subtitle != null && (positionUs < subtitle.getStartTime() + || subtitle.getLastEventTime() <= positionUs)) { subtitle = null; } parserHelper.flush(); clearTextRenderer(); - syncNextEventIndex(timeUs); + syncNextEventIndex(positionUs); textRendererNeedsUpdate = subtitle != null; } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { try { - source.continueBuffering(timeUs); + source.continueBuffering(positionUs); } catch (IOException e) { throw new ExoPlaybackException(e); } - currentPositionUs = timeUs; + currentPositionUs = positionUs; if (parserHelper.isParsing()) { return; @@ -169,13 +169,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { if (subtitle == null && dequeuedSubtitle != null) { // We've dequeued a new subtitle. Sync the event index and update the subtitle. subtitle = dequeuedSubtitle; - syncNextEventIndex(timeUs); + syncNextEventIndex(positionUs); textRendererNeedsUpdate = true; } else if (subtitle != null) { // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we // advance to the next event. long nextEventTimeUs = getNextEventTime(); - while (nextEventTimeUs <= timeUs) { + while (nextEventTimeUs <= positionUs) { nextSubtitleEventIndex++; nextEventTimeUs = getNextEventTime(); textRendererNeedsUpdate = true; @@ -191,7 +191,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { if (subtitle == null) { try { SampleHolder sampleHolder = parserHelper.getSampleHolder(); - int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false); + int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { parserHelper.startParseOperation(); } else if (result == SampleSource.END_OF_STREAM) { @@ -208,7 +208,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { if (subtitle == null) { clearTextRenderer(); } else { - updateTextRenderer(timeUs); + updateTextRenderer(positionUs); } } } @@ -256,8 +256,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { return true; } - private void syncNextEventIndex(long timeUs) { - nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(timeUs); + private void syncNextEventIndex(long positionUs) { + nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(positionUs); } private long getNextEventTime() { @@ -266,8 +266,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { : (subtitle.getEventTime(nextSubtitleEventIndex)); } - private void updateTextRenderer(long timeUs) { - String text = subtitle.getText(timeUs); + private void updateTextRenderer(long positionUs) { + String text = subtitle.getText(positionUs); log("updateTextRenderer; text=: " + text); if (textRendererHandler != null) { textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget(); From 759431048d7c990c2c3ef8a28e73c8e3e843b680 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:27:20 +0100 Subject: [PATCH 040/110] Treat "no chunk to load yet" in the same way as finished. The key change here is that nextLoadPositionUs is set to -1 if we're not loading but don't have a next chunk ready to load. This ensures that "missing chunks" in one stream don't prevent chunks in another stream from loading. This occurs in SmoothStreaming with TTML subtitles, where the chunks are sparse. --- .../android/exoplayer/DefaultLoadControl.java | 12 +-- .../google/android/exoplayer/LoadControl.java | 5 +- .../exoplayer/chunk/ChunkSampleSource.java | 73 ++++++++++++------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java index 91bcac53ce..9131c4816c 100644 --- a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java @@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl { // Update the loader state. int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs); LoaderState loaderState = loaderStates.get(loader); - boolean loaderStateChanged = loaderState.bufferState != loaderBufferState || - loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading || - loaderState.failed != failed; + boolean loaderStateChanged = loaderState.bufferState != loaderBufferState + || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading + || loaderState.failed != failed; if (loaderStateChanged) { loaderState.bufferState = loaderBufferState; loaderState.nextLoadPositionUs = nextLoadPositionUs; @@ -214,17 +214,17 @@ public class DefaultLoadControl implements LoadControl { private void updateControlState() { boolean loading = false; boolean failed = false; - boolean finished = true; + boolean haveNextLoadPosition = false; int highestState = bufferPoolState; for (int i = 0; i < loaders.size(); i++) { LoaderState loaderState = loaderStates.get(loaders.get(i)); loading |= loaderState.loading; failed |= loaderState.failed; - finished &= loaderState.nextLoadPositionUs == -1; + haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1; highestState = Math.max(highestState, loaderState.bufferState); } - fillingBuffers = !loaders.isEmpty() && !finished && !failed + fillingBuffers = !loaders.isEmpty() && !failed && (loading || haveNextLoadPosition) && (highestState == BELOW_LOW_WATERMARK || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); if (fillingBuffers && !streamingPrioritySet) { diff --git a/library/src/main/java/com/google/android/exoplayer/LoadControl.java b/library/src/main/java/com/google/android/exoplayer/LoadControl.java index edc6ff023f..df6130017f 100644 --- a/library/src/main/java/com/google/android/exoplayer/LoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/LoadControl.java @@ -65,9 +65,10 @@ public interface LoadControl { * * @param loader The loader invoking the update. * @param playbackPositionUs The loader's playback position. - * @param nextLoadPositionUs The loader's next load position, or -1 if finished. + * @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the + * next load position is not yet known. * @param loading Whether the loader is currently loading data. - * @param failed Whether the loader has failed, meaning it does not wish to load more data. + * @param failed Whether the loader has failed. * @return True if the loader is allowed to start its next load. False otherwise. */ boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, 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 a436077d5f..f7d556f3c8 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 @@ -499,23 +499,40 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void updateLoadControl() { - long loadPositionUs; - if (isPendingReset()) { - loadPositionUs = pendingResetPositionUs; - } else { - MediaChunk lastMediaChunk = mediaChunks.getLast(); - loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; - } - - boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal; - boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, - isBackedOff || loader.isLoading(), currentLoadableExceptionFatal); - if (currentLoadableExceptionFatal) { + // We've failed, but we still need to update the control with our current state. + loadControl.update(this, downstreamPositionUs, -1, false, true); return; } long now = SystemClock.elapsedRealtime(); + long nextLoadPositionUs = getNextLoadPositionUs(); + boolean isBackedOff = currentLoadableException != null; + boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; + + // If we're not loading or backed off, evaluate the operation if (a) we don't have the next + // chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago. + if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) + || (now - lastPerformedBufferOperation > 2000))) { + // Perform the evaluation. + lastPerformedBufferOperation = now; + currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); + boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); + // Update the next load position as appropriate. + if (currentLoadableHolder.chunk == null) { + // Set loadPosition to -1 to indicate that we don't have anything to load. + nextLoadPositionUs = -1; + } else if (chunksDiscarded) { + // Chunks were discarded, so we need to re-evaluate the load position. + nextLoadPositionUs = getNextLoadPositionUs(); + } + } + + // Update the control with our current state, and determine whether we're the next loader. + boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, + loadingOrBackedOff, false); if (isBackedOff) { long elapsedMillis = now - currentLoadableExceptionTimestamp; @@ -525,17 +542,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { return; } - if (!loader.isLoading()) { - if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { - lastPerformedBufferOperation = now; - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, - downstreamPositionUs, currentLoadableHolder); - discardUpstreamMediaChunks(currentLoadableHolder.queueSize); - } - if (nextLoader) { - maybeStartLoading(); - } + if (!loader.isLoading() && nextLoader) { + maybeStartLoading(); + } + } + + /** + * Gets the next load time, assuming that the next load starts where the previous chunk ended (or + * from the pending reset time, if there is one). + */ + private long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + MediaChunk lastMediaChunk = mediaChunks.getLast(); + return lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; } } @@ -652,10 +673,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { * Discard upstream media chunks until the queue length is equal to the length specified. * * @param queueLength The desired length of the queue. + * @return True if chunks were discarded. False otherwise. */ - private void discardUpstreamMediaChunks(int queueLength) { + private boolean discardUpstreamMediaChunks(int queueLength) { if (mediaChunks.size() <= queueLength) { - return; + return false; } long totalBytes = 0; long startTimeUs = 0; @@ -667,6 +689,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { removed.release(); } notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes); + return true; } private boolean isMediaChunk(Chunk chunk) { From fd3016cd13805150c0dac32220b328be071c8c51 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:54:30 +0100 Subject: [PATCH 041/110] Use setVolume on API level 21 devices, plus minor naming cleanup. --- .../MediaCodecAudioTrackRenderer.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 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 25bc82646a..5cba5dd99a 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -333,7 +333,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM, audioSessionId); checkAudioTrackInitialized(); } - audioTrack.setStereoVolume(volume, volume); + setVolume(volume); if (getState() == TrackRenderer.STATE_STARTED) { audioTrackResumeSystemTimeUs = System.nanoTime() / 1000; audioTrack.play(); @@ -516,7 +516,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { - audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack); + audioTimestampSet = audioTimestampCompat.update(audioTrack); if (audioTimestampSet) { // Perform sanity checks on the timestamp. long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000; @@ -713,10 +713,24 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { private void setVolume(float volume) { this.volume = volume; if (audioTrack != null) { - audioTrack.setStereoVolume(volume, volume); + if (Util.SDK_INT >= 21) { + setVolumeV21(audioTrack, volume); + } else { + setVolumeV3(audioTrack, volume); + } } } + @TargetApi(21) + private static void setVolumeV21(AudioTrack audioTrack, float volume) { + audioTrack.setVolume(volume); + } + + @SuppressWarnings("deprecation") + private static void setVolumeV3(AudioTrack audioTrack, float volume) { + audioTrack.setStereoVolume(volume, volume); + } + private void notifyAudioTrackInitializationError(final AudioTrackInitializationException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @@ -736,7 +750,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { /** * Returns true if the audioTimestamp was retrieved from the audioTrack. */ - boolean initTimestamp(AudioTrack audioTrack); + boolean update(AudioTrack audioTrack); long getNanoTime(); @@ -750,7 +764,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { private static final class NoopAudioTimestampCompat implements AudioTimestampCompat { @Override - public boolean initTimestamp(AudioTrack audioTrack) { + public boolean update(AudioTrack audioTrack) { return false; } @@ -782,7 +796,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @Override - public boolean initTimestamp(AudioTrack audioTrack) { + public boolean update(AudioTrack audioTrack) { return audioTrack.getTimestamp(audioTimestamp); } From fcd9ec6c23a556cea61ae1c7145471d4e18ed0f7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 21:55:10 +0100 Subject: [PATCH 042/110] Tweak audio renderer to match dev/dev-hls. --- .../android/exoplayer/MediaCodecAudioTrackRenderer.java | 7 +++++-- 1 file changed, 5 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 5cba5dd99a..a9e201e008 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -576,9 +576,12 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onDisabled() { - super.onDisabled(); - releaseAudioTrack(); audioSessionId = 0; + try { + releaseAudioTrack(); + } finally { + super.onDisabled(); + } } @Override From 48536118039c46787836287aa7ed9f06710906a1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Oct 2014 16:45:56 +0100 Subject: [PATCH 043/110] Remove additional "/" from merged URLs. Issue: #81 --- .../src/main/java/com/google/android/exoplayer/util/Util.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index e7984ef72b..78c2d267d6 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -167,6 +167,7 @@ public final class Util { return Uri.parse(stringUri); } if (stringUri.startsWith("/")) { + stringUri = stringUri.substring(1); return new Uri.Builder() .scheme(baseUri.getScheme()) .authority(baseUri.getAuthority()) From 5a8713321914344286527fc8fd8a343074b8b650 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Oct 2014 16:54:51 +0100 Subject: [PATCH 044/110] Add case for 7.1 audio. --- .../google/android/exoplayer/MediaCodecAudioTrackRenderer.java | 3 +++ 1 file changed, 3 insertions(+) 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 5027cb7830..d61f044b42 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -294,6 +294,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { case 6: channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; break; + case 8: + channelConfig = AudioFormat.CHANNEL_OUT_7POINT1; + break; default: throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } From 1f0d41188656acb619014dde3f6f01a980f298ae Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Oct 2014 16:55:38 +0100 Subject: [PATCH 045/110] Make mpd parser more ameanable for extension. --- .../exoplayer/dash/mpd/AdaptationSet.java | 2 +- .../exoplayer/dash/mpd/ContentProtection.java | 21 +-- .../mpd/MediaPresentationDescription.java | 2 +- .../MediaPresentationDescriptionParser.java | 130 +++++++++++++++--- .../android/exoplayer/dash/mpd/Period.java | 2 +- 5 files changed, 114 insertions(+), 43 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java index db68df9e13..e7a9f02828 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java @@ -21,7 +21,7 @@ import java.util.List; /** * Represents a set of interchangeable encoded versions of a media content component. */ -public final class AdaptationSet { +public class AdaptationSet { public static final int TYPE_UNKNOWN = -1; public static final int TYPE_VIDEO = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java index 1232f804f3..bd6acca9af 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java @@ -15,36 +15,21 @@ */ package com.google.android.exoplayer.dash.mpd; -import java.util.Collections; -import java.util.Map; - /** - * Represents a ContentProtection tag in an AdaptationSet. Holds arbitrary data for various DRM - * schemes. + * Represents a ContentProtection tag in an AdaptationSet. */ -public final class ContentProtection { +public class ContentProtection { /** * Identifies the content protection scheme. */ public final String schemeUriId; - /** - * Protection scheme specific data. - */ - public final Map keyedData; /** * @param schemeUriId Identifies the content protection scheme. - * @param keyedData Data specific to the scheme. */ - public ContentProtection(String schemeUriId, Map keyedData) { + public ContentProtection(String schemeUriId) { this.schemeUriId = schemeUriId; - if (keyedData != null) { - this.keyedData = Collections.unmodifiableMap(keyedData); - } else { - this.keyedData = Collections.emptyMap(); - } - } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java index 98e85ac40b..b1946645c7 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java @@ -21,7 +21,7 @@ import java.util.List; /** * Represents a DASH media presentation description (mpd). */ -public final class MediaPresentationDescription { +public class MediaPresentationDescription { public final long availabilityStartTime; 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 606f91a4ef..de5fc9bdb6 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 @@ -77,7 +77,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } - private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, + protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException { long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1); long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1); @@ -101,17 +101,29 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } while (!isEndTag(xpp, "MPD")); + return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods); + } + + protected MediaPresentationDescription buildMediaPresentationDescription( + long availabilityStartTime, long durationMs, long minBufferTimeMs, boolean dynamic, + long minUpdateTimeMs, long timeShiftBufferDepthMs, UtcTimingElement utcTiming, + List periods) { return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods); } - private UtcTimingElement parseUtcTiming(XmlPullParser xpp) { + protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); String value = xpp.getAttributeValue(null, "value"); + return buildUtcTimingElement(schemeIdUri, value); + } + + protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) { return new UtcTimingElement(schemeIdUri, value); } - private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) + protected Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", 0); @@ -134,12 +146,17 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } while (!isEndTag(xpp, "Period")); + return buildPeriod(id, startMs, durationMs, adaptationSets); + } + + protected Period buildPeriod( + String id, long startMs, long durationMs, List adaptationSets) { return new Period(id, startMs, durationMs, adaptationSets); } // AdaptationSet parsing. - private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl, + protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl, long periodStartMs, long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { @@ -176,13 +193,20 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } else if (isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, periodDurationMs); + } else if (isStartTag(xpp)) { + parseAdaptationSetChild(xpp); } } while (!isEndTag(xpp, "AdaptationSet")); + return buildAdaptationSet(id, contentType, representations, contentProtections); + } + + protected AdaptationSet buildAdaptationSet(int id, int contentType, + List representations, List contentProtections) { return new AdaptationSet(id, contentType, representations, contentProtections); } - private int parseAdaptationSetType(String contentType) { + protected int parseAdaptationSetType(String contentType) { return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO @@ -190,7 +214,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler : AdaptationSet.TYPE_UNKNOWN; } - private int parseAdaptationSetTypeFromMimeType(String mimeType) { + protected int parseAdaptationSetTypeFromMimeType(String mimeType) { return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN : MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO : MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO @@ -228,13 +252,29 @@ public class MediaPresentationDescriptionParser extends DefaultHandler **/ protected ContentProtection parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { - String schemeUriId = xpp.getAttributeValue(null, "schemeUriId"); - return new ContentProtection(schemeUriId, null); + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + return buildContentProtection(schemeIdUri); + } + + protected ContentProtection buildContentProtection(String schemeIdUri) { + return new ContentProtection(schemeIdUri); + } + + /** + * Parses children of AdaptationSet elements not specifically parsed elsewhere. + * + * @param xpp The XmpPullParser from which the AdaptationSet child should be parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + **/ + protected void parseAdaptationSetChild(XmlPullParser xpp) + throws XmlPullParserException, IOException { + // pass } // Representation parsing. - private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, + protected Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, long periodStartMs, long periodDurationMs, String mimeType, String language, SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); @@ -261,15 +301,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } while (!isEndTag(xpp, "Representation")); - Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, + Format format = buildFormat(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth, language); - return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format, + return buildRepresentation(periodStartMs, periodDurationMs, contentId, -1, format, segmentBase); } + protected Format buildFormat(String id, String mimeType, int width, int height, int numChannels, + int audioSamplingRate, int bandwidth, String language) { + return new Format(id, mimeType, width, height, numChannels, audioSamplingRate, + bandwidth, language); + } + + protected Representation buildRepresentation(long periodStartMs, long periodDurationMs, + String contentId, int revisionId, Format format, SegmentBase segmentBase) { + return Representation.newInstance(periodStartMs, periodDurationMs, contentId, revisionId, + format, segmentBase); + } + // SegmentBase, SegmentList and SegmentTemplate parsing. - private SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl, + protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl, SingleSegmentBase parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -293,11 +345,17 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } while (!isEndTag(xpp, "SegmentBase")); + return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, + indexStart, indexLength); + } + + protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale, + long presentationTimeOffset, Uri baseUrl, long indexStart, long indexLength) { return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, indexStart, indexLength); } - private SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent, + protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent, long periodDuration) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -330,11 +388,18 @@ public class MediaPresentationDescriptionParser extends DefaultHandler segments = segments != null ? segments : parent.mediaSegments; } + return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDuration, + startNumber, duration, timeline, segments); + } + + protected SegmentList buildSegmentList(RangedUri initialization, long timescale, + long presentationTimeOffset, long periodDuration, int startNumber, long duration, + List timeline, List segments) { return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration, startNumber, duration, timeline, segments); } - private SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl, + protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl, SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -364,11 +429,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler timeline = timeline != null ? timeline : parent.segmentTimeline; } + return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, + startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); + } + + protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, + long presentationTimeOffset, long periodDuration, int startNumber, long duration, + List timeline, UrlTemplate initializationTemplate, + UrlTemplate mediaTemplate, Uri baseUrl) { return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } - private List parseSegmentTimeline(XmlPullParser xpp) + protected List parseSegmentTimeline(XmlPullParser xpp) throws XmlPullParserException, IOException { List segmentTimeline = new ArrayList(); long elapsedTime = 0; @@ -379,7 +452,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler long duration = parseLong(xpp, "d"); int count = 1 + parseInt(xpp, "r", 0); for (int i = 0; i < count; i++) { - segmentTimeline.add(new SegmentTimelineElement(elapsedTime, duration)); + segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration)); elapsedTime += duration; } } @@ -387,7 +460,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return segmentTimeline; } - private UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name, + protected SegmentTimelineElement buildSegmentTimelineElement(long elapsedTime, long duration) { + return new SegmentTimelineElement(elapsedTime, duration); + } + + protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name, UrlTemplate defaultValue) { String valueString = xpp.getAttributeValue(null, name); if (valueString != null) { @@ -396,15 +473,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return defaultValue; } - private RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) { + protected RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) { return parseRangedUrl(xpp, baseUrl, "sourceURL", "range"); } - private RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) { + protected RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) { return parseRangedUrl(xpp, baseUrl, "media", "mediaRange"); } - private RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute, + protected RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute, String rangeAttribute) { String urlText = xpp.getAttributeValue(null, urlAttribute); long rangeStart = 0; @@ -415,6 +492,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler rangeStart = Long.parseLong(rangeTextArray[0]); rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1; } + return buildRangedUri(baseUrl, urlText, rangeStart, rangeLength); + } + + protected RangedUri buildRangedUri(Uri baseUrl, String urlText, long rangeStart, + long rangeLength) { return new RangedUri(baseUrl, urlText, rangeStart, rangeLength); } @@ -429,7 +511,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName()); } - private static long parseDuration(XmlPullParser xpp, String name, long defaultValue) { + protected static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException { + return xpp.getEventType() == XmlPullParser.START_TAG; + } + + protected static long parseDuration(XmlPullParser xpp, String name, long defaultValue) { String value = xpp.getAttributeValue(null, name); if (value == null) { return defaultValue; @@ -438,7 +524,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } - private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) + protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) throws ParseException { String value = xpp.getAttributeValue(null, name); if (value == null) { diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java index 6fd3a71f4f..3e64cb9853 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java @@ -21,7 +21,7 @@ import java.util.List; /** * Encapsulates media content components over a contiguous period of time. */ -public final class Period { +public class Period { /** * The period identifier, if one exists. From b8415dba599ef681f371f25b2fc9df21ed2bc9e0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 10:51:45 +0000 Subject: [PATCH 046/110] Parse all UUID boxes, not just the first one. --- .../exoplayer/parser/mp4/FragmentedMp4Extractor.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 0fc1e1b200..a1994f7c67 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 @@ -751,9 +751,12 @@ public final class FragmentedMp4Extractor implements Extractor { parseSenc(senc.data, out); } - LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid); - if (uuid != null) { - parseUuid(uuid.data, out, extendedTypeScratch); + int childrenSize = traf.children.size(); + for (int i = 0; i < childrenSize; i++) { + Atom atom = traf.children.get(i); + if (atom.type == Atom.TYPE_uuid) { + parseUuid(((LeafAtom) atom).data, out, extendedTypeScratch); + } } } From f859205438a496c7859233e5880b086984bd58d7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 10:52:45 +0000 Subject: [PATCH 047/110] Let FileDataSource report to a TransferListener. --- .../exoplayer/upstream/FileDataSource.java | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) 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 a08cacb982..ec9a3b9ade 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 @@ -36,8 +36,27 @@ public final class FileDataSource implements DataSource { } + private final TransferListener listener; + private RandomAccessFile file; private long bytesRemaining; + private boolean opened; + + /** + * Constructs a new {@link DataSource} that retrieves data from a file. + */ + public FileDataSource() { + this(null); + } + + /** + * Constructs a new {@link DataSource} that retrieves data from a file. + * + * @param listener An optional listener. Specify {@code null} for no listener. + */ + public FileDataSource(TransferListener listener) { + this.listener = listener; + } @Override public long open(DataSpec dataSpec) throws FileDataSourceException { @@ -46,10 +65,16 @@ public final class FileDataSource implements DataSource { file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position : dataSpec.length; - return bytesRemaining; } catch (IOException e) { throw new FileDataSourceException(e); } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + + return bytesRemaining; } @Override @@ -63,7 +88,14 @@ public final class FileDataSource implements DataSource { } catch (IOException e) { throw new FileDataSourceException(e); } - bytesRemaining -= bytesRead; + + if (bytesRead > 0) { + bytesRemaining -= bytesRead; + if (listener != null) { + listener.onBytesTransferred(bytesRead); + } + } + return bytesRead; } } @@ -75,8 +107,16 @@ public final class FileDataSource implements DataSource { file.close(); } catch (IOException e) { throw new FileDataSourceException(e); + } finally { + file = null; + + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } } - file = null; } } From 6aeb989327a9cdd1dd2ce9d924083310e825f522 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 10:53:39 +0000 Subject: [PATCH 048/110] Add some MimeTypes that will be useful in the future. --- .../main/java/com/google/android/exoplayer/util/MimeTypes.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 63d5220ac1..e3443467f3 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -32,6 +32,8 @@ public class MimeTypes { public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm"; + public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; + public static final String AUDIO_EC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From ae6e082d2f4ed4304211a9c818afd81e5b8a7388 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 10:54:51 +0000 Subject: [PATCH 049/110] Add a UriDataSource for reading from file or network. --- .../exoplayer/upstream/UriDataSource.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java new file mode 100644 index 0000000000..1243576b17 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java @@ -0,0 +1,85 @@ +/* + * 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.upstream; + +import com.google.android.exoplayer.util.Assertions; + +import java.io.IOException; + +/** + * A data source that fetches data from a local or remote {@link DataSpec}. + */ +public final class UriDataSource implements DataSource { + + private static final String FILE_URI_SCHEME = "file"; + + private final DataSource fileDataSource; + private final DataSource httpDataSource; + + /** + * {@code null} if no data source is open. Otherwise, equal to {@link #fileDataSource} if the open + * data source is a file, or {@link #httpDataSource} otherwise. + */ + private DataSource dataSource; + + /** + * Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and an + * {@link HttpDataSource} for other URIs. + * + * @param userAgent The User-Agent string that should be used when requesting remote data. + * @param transferListener An optional listener. + */ + public UriDataSource(String userAgent, TransferListener transferListener) { + this(new FileDataSource(transferListener), + new HttpDataSource(userAgent, null, transferListener)); + } + + /** + * Constructs a new data source using {@code fileDataSource} for file URIs, and + * {@code httpDataSource} for non-file URIs. + * + * @param fileDataSource {@link DataSource} to use for file URIs. + * @param httpDataSource {@link DataSource} to use for non-file URIs. + */ + public UriDataSource(DataSource fileDataSource, DataSource httpDataSource) { + this.fileDataSource = Assertions.checkNotNull(fileDataSource); + this.httpDataSource = Assertions.checkNotNull(httpDataSource); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + Assertions.checkState(dataSource == null); + + dataSource = dataSpec.uri.getScheme().equals(FILE_URI_SCHEME) ? fileDataSource : httpDataSource; + return dataSource.open(dataSpec); + } + + @Override + public void close() throws IOException { + Assertions.checkNotNull(dataSource); + + dataSource.close(); + dataSource = null; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + Assertions.checkNotNull(dataSource); + + return dataSource.read(buffer, offset, readLength); + } + +} From 5f6b197355e27e5bad497f1c9859c4f22e8824b5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 10:56:52 +0000 Subject: [PATCH 050/110] Allow direct and indirect buffer replacement. Also tweak ManifestFetcher. --- .../exoplayer/MediaCodecTrackRenderer.java | 2 +- .../android/exoplayer/SampleHolder.java | 43 ++++++++++++++++--- .../chunk/SingleSampleMediaChunk.java | 6 +-- .../parser/mp4/FragmentedMp4Extractor.java | 7 ++- .../exoplayer/parser/webm/WebmExtractor.java | 8 ++-- .../exoplayer/text/SubtitleParserHelper.java | 2 +- .../exoplayer/util/ManifestFetcher.java | 10 ++--- 7 files changed, 52 insertions(+), 26 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 5e28f36d1b..7904977dee 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -174,7 +174,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { this.eventHandler = eventHandler; this.eventListener = eventListener; codecCounters = new CodecCounters(); - sampleHolder = new SampleHolder(false); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); formatHolder = new MediaFormatHolder(); decodeOnlyPresentationTimestamps = new HashSet(); outputBufferInfo = new MediaCodec.BufferInfo(); diff --git a/library/src/main/java/com/google/android/exoplayer/SampleHolder.java b/library/src/main/java/com/google/android/exoplayer/SampleHolder.java index 6518b06ac5..43308bc40b 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleHolder.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleHolder.java @@ -23,10 +23,19 @@ import java.nio.ByteBuffer; public final class SampleHolder { /** - * Whether a {@link SampleSource} is permitted to replace {@link #data} if its current value is - * null or of insufficient size to hold the sample. + * Disallows buffer replacement. */ - public final boolean allowDataBufferReplacement; + public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0; + + /** + * Allows buffer replacement using {@link ByteBuffer#allocate(int)}. + */ + public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1; + + /** + * Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}. + */ + public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2; public final CryptoInfo cryptoInfo; @@ -57,12 +66,34 @@ public final class SampleHolder { */ public boolean decodeOnly; + private final int bufferReplacementMode; + /** - * @param allowDataBufferReplacement See {@link #allowDataBufferReplacement}. + * @param bufferReplacementMode Determines the behavior of {@link #replaceBuffer(int)}. One of + * {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and + * {@link #BUFFER_REPLACEMENT_MODE_DIRECT}. */ - public SampleHolder(boolean allowDataBufferReplacement) { + public SampleHolder(int bufferReplacementMode) { this.cryptoInfo = new CryptoInfo(); - this.allowDataBufferReplacement = allowDataBufferReplacement; + this.bufferReplacementMode = bufferReplacementMode; + } + + /** + * Attempts to replace {@link #data} with a {@link ByteBuffer} of the specified capacity. + * + * @param capacity The capacity of the replacement buffer, in bytes. + * @return True if the buffer was replaced. False otherwise. + */ + public boolean replaceBuffer(int capacity) { + switch (bufferReplacementMode) { + case BUFFER_REPLACEMENT_MODE_NORMAL: + data = ByteBuffer.allocate(capacity); + return true; + case BUFFER_REPLACEMENT_MODE_DIRECT: + data = ByteBuffer.allocateDirect(capacity); + return true; + } + return false; } } 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 e0b9f91ad0..f097d9ee32 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 @@ -22,7 +22,6 @@ import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; -import java.nio.ByteBuffer; import java.util.Map; import java.util.UUID; @@ -97,9 +96,8 @@ public class SingleSampleMediaChunk extends MediaChunk { if (headerData != null) { sampleSize += headerData.length; } - if (holder.allowDataBufferReplacement && - (holder.data == null || holder.data.capacity() < sampleSize)) { - holder.data = ByteBuffer.allocate(sampleSize); + if (holder.data == null || holder.data.capacity() < sampleSize) { + holder.replaceBuffer(sampleSize); } int bytesRead; if (holder.data != null) { 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 a1994f7c67..e6bf68f30a 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 @@ -1069,21 +1069,20 @@ public final class FragmentedMp4Extractor implements Extractor { if (out == null) { return RESULT_NEED_SAMPLE_HOLDER; } - ByteBuffer outputData = out.data; out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; out.flags = 0; if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; lastSyncSampleIndex = sampleIndex; } - if (out.allowDataBufferReplacement && (out.data == null || out.data.capacity() < sampleSize)) { - outputData = ByteBuffer.allocate(sampleSize); - out.data = outputData; + if (out.data == null || out.data.capacity() < sampleSize) { + out.replaceBuffer(sampleSize); } if (fragmentRun.definesEncryptionData) { readSampleEncryptionData(fragmentRun.sampleEncryptionData, out); } + ByteBuffer outputData = out.data; if (outputData == null) { inputStream.skip(sampleSize); out.size = 0; 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 a0e0b962b3..7a44d1d960 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 @@ -347,13 +347,11 @@ public final class WebmExtractor implements Extractor { throw new IllegalStateException("Lacing mode " + lacing + " not supported"); } - ByteBuffer outputData = sampleHolder.data; - if (sampleHolder.allowDataBufferReplacement - && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) { - outputData = ByteBuffer.allocate(sampleHolder.size); - sampleHolder.data = outputData; + if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) { + sampleHolder.replaceBuffer(sampleHolder.size); } + ByteBuffer outputData = sampleHolder.data; if (outputData == null) { reader.skipBytes(inputStream, sampleHolder.size); sampleHolder.size = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java index cafdb89f25..38958aa0b3 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java @@ -55,7 +55,7 @@ public class SubtitleParserHelper implements Handler.Callback { * Flushes the helper, canceling the current parsing operation, if there is one. */ public synchronized void flush() { - sampleHolder = new SampleHolder(true); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); parsing = false; result = null; error = null; diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 423b1d0204..c72a856919 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -24,8 +24,8 @@ import android.util.Pair; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLConnection; import java.util.concurrent.CancellationException; /** @@ -282,10 +282,10 @@ public class ManifestFetcher implements Loader.Callback { @Override public void load() throws IOException, InterruptedException { - String inputEncoding = null; + String inputEncoding; InputStream inputStream = null; try { - HttpURLConnection connection = configureHttpConnection(new URL(manifestUrl)); + URLConnection connection = configureConnection(new URL(manifestUrl)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); result = parser.parse(inputStream, inputEncoding, contentId, @@ -297,8 +297,8 @@ public class ManifestFetcher implements Loader.Callback { } } - private HttpURLConnection configureHttpConnection(URL url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + private URLConnection configureConnection(URL url) throws IOException { + URLConnection connection = url.openConnection(); connection.setConnectTimeout(TIMEOUT_MILLIS); connection.setReadTimeout(TIMEOUT_MILLIS); connection.setDoOutput(false); From 067422a491da0fe5955a98946961bf3827b90b74 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 10:59:37 +0000 Subject: [PATCH 051/110] Cleanup TextTrackRenderer. --- .../exoplayer/text/TextTrackRenderer.java | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) 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 f7f38d986a..26ed0145c4 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 @@ -21,7 +21,6 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.VerboseLogUtil; import android.annotation.TargetApi; import android.os.Handler; @@ -29,7 +28,6 @@ import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.util.Log; import java.io.IOException; @@ -54,8 +52,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } - private static final String TAG = "TextTrackRenderer"; - private static final int MSG_UPDATE_OVERLAY = 0; private final Handler textRendererHandler; @@ -145,14 +141,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { @Override protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + currentPositionUs = positionUs; try { source.continueBuffering(positionUs); } catch (IOException e) { throw new ExoPlaybackException(e); } - currentPositionUs = positionUs; - if (parserHelper.isParsing()) { return; } @@ -188,7 +183,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { // We don't have a subtitle. Try and read the next one from the source, and if we succeed then // sync and set textRendererNeedsUpdate. - if (subtitle == null) { + if (!inputStreamEnded && subtitle == null) { try { SampleHolder sampleHolder = parserHelper.getSampleHolder(); int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); @@ -215,12 +210,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { @Override protected void onDisabled() { - source.disable(trackIndex); subtitle = null; parserThread.quit(); parserThread = null; parserHelper = null; clearTextRenderer(); + source.disable(trackIndex); } @Override @@ -268,20 +263,18 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { private void updateTextRenderer(long positionUs) { String text = subtitle.getText(positionUs); - log("updateTextRenderer; text=: " + text); if (textRendererHandler != null) { textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget(); } else { - invokeTextRenderer(text); + invokeRendererInternal(text); } } private void clearTextRenderer() { - log("clearTextRenderer"); if (textRendererHandler != null) { textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget(); } else { - invokeTextRenderer(null); + invokeRendererInternal(null); } } @@ -289,20 +282,14 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_OVERLAY: - invokeTextRenderer((String) msg.obj); + invokeRendererInternal((String) msg.obj); return true; } return false; } - private void invokeTextRenderer(String text) { + private void invokeRendererInternal(String text) { textRenderer.onText(text); } - private void log(String logMessage) { - if (VerboseLogUtil.isTagEnabled(TAG)) { - Log.v(TAG, logMessage); - } - } - } From 192cdc66a2ba13292f3cfd7090dc8c810302e53f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 11:00:04 +0000 Subject: [PATCH 052/110] Ignore secure decoders. They shouldn't be explicitly listed. --- .../java/com/google/android/exoplayer/MediaCodecUtil.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 31ec2ae9f1..9da62a59d2 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -76,7 +76,7 @@ public class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); String codecName = info.getName(); - if (!info.isEncoder() && isOmxCodec(codecName)) { + if (!info.isEncoder() && codecName.startsWith("OMX.") && !codecName.endsWith(".secure")) { String[] supportedTypes = info.getSupportedTypes(); for (int j = 0; j < supportedTypes.length; j++) { String supportedType = supportedTypes[j]; @@ -91,10 +91,6 @@ public class MediaCodecUtil { return null; } - private static boolean isOmxCodec(String name) { - return name.startsWith("OMX."); - } - private static boolean isAdaptive(CodecCapabilities capabilities) { if (Util.SDK_INT >= 19) { return isAdaptiveV19(capabilities); From b5c4148f8f587cf19abb4e611fa726b58bdf6cb2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 27 Oct 2014 11:20:39 +0000 Subject: [PATCH 053/110] Use UriDataSource in demo app. --- .../demo/full/player/DashVodRendererBuilder.java | 6 +++--- .../demo/full/player/SmoothStreamingRendererBuilder.java | 8 ++++---- .../exoplayer/demo/simple/DashVodRendererBuilder.java | 6 +++--- .../demo/simple/SmoothStreamingRendererBuilder.java | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 32d60beafc..d6dff9e60e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -43,7 +43,7 @@ import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.MimeTypes; @@ -162,7 +162,7 @@ public class DashVodRendererBuilder implements RendererBuilder, } // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); ChunkSource videoChunkSource; String mimeType = videoRepresentations[0].format.mimeType; if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) { @@ -187,7 +187,7 @@ public class DashVodRendererBuilder implements RendererBuilder, audioChunkSource = null; audioRenderer = null; } else { - DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); audioTrackNames = new String[audioRepresentationsList.size()]; ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 940691b48c..9922bab839 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -41,7 +41,7 @@ import com.google.android.exoplayer.text.ttml.TtmlParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.Util; @@ -155,7 +155,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, videoStreamElementIndex, videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); @@ -177,7 +177,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } else { audioTrackNames = new String[audioStreamElementCount]; ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount]; - DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator(); audioStreamElementCount = 0; for (int i = 0; i < manifest.streamElements.length; i++) { @@ -208,7 +208,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } else { textTrackNames = new String[textStreamElementCount]; ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount]; - DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource ttmlDataSource = new UriDataSource(userAgent, bandwidthMeter); FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator(); textStreamElementCount = 0; for (int i = 0; i < manifest.streamElements.length; i++) { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index 4428fba201..fda21da54f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -37,7 +37,7 @@ import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBui import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -118,7 +118,7 @@ import java.util.ArrayList; videoRepresentationsList.toArray(videoRepresentations); // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); ChunkSource videoChunkSource = new DashChunkSource(videoDataSource, new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, @@ -127,7 +127,7 @@ import java.util.ArrayList; MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); // Build the audio renderer. - DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); ChunkSource audioChunkSource = new DashChunkSource(audioDataSource, new FormatEvaluator.FixedEvaluator(), audioRepresentation); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index eb80499ebd..4d92c0458c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -35,7 +35,7 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParse import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -121,7 +121,7 @@ import java.util.ArrayList; } // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, videoStreamElementIndex, videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); @@ -131,7 +131,7 @@ import java.util.ArrayList; MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); // Build the audio renderer. - DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher, audioStreamElementIndex, new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS); From c34f7368aeb28d21a9255e995f8b0e346dc61527 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 14:12:55 +0000 Subject: [PATCH 054/110] Minor tweak to UriDataSource. --- .../exoplayer/upstream/UriDataSource.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java index 1243576b17..0655381191 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java @@ -62,24 +62,21 @@ public final class UriDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { Assertions.checkState(dataSource == null); - - dataSource = dataSpec.uri.getScheme().equals(FILE_URI_SCHEME) ? fileDataSource : httpDataSource; + dataSource = FILE_URI_SCHEME.equals(dataSpec.uri.getScheme()) ? fileDataSource : httpDataSource; return dataSource.open(dataSpec); } - @Override - public void close() throws IOException { - Assertions.checkNotNull(dataSource); - - dataSource.close(); - dataSource = null; - } - @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { - Assertions.checkNotNull(dataSource); - return dataSource.read(buffer, offset, readLength); } + @Override + public void close() throws IOException { + if (dataSource != null) { + dataSource.close(); + dataSource = null; + } + } + } From 552db2fa7c8e83ee934f490e4873154789fdf367 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 14:15:52 +0000 Subject: [PATCH 055/110] Avoid spurious preparing->idle->preparing transition in demo app. Issue #81 --- .../google/android/exoplayer/demo/full/player/DemoPlayer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 914c9f9798..b88ce89157 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 @@ -131,7 +131,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi * A listener for receiving notifications of timed text. */ public interface TextListener { - public abstract void onText(String text); + void onText(String text); } // Constants pulled into this class for convenience. @@ -287,7 +287,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi this.trackNames = trackNames; this.multiTrackSources = multiTrackSources; rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; - maybeReportPlayerState(); pushSurfaceAndVideoTrack(false); pushTrackSelection(TYPE_AUDIO, true); pushTrackSelection(TYPE_TEXT, true); From 11cbe2819eceef362be7103d16698ff2f62b7bcb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 17:55:21 +0000 Subject: [PATCH 056/110] Clean up project files. --- demo/src/main/.classpath | 2 +- library/src/main/.project | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/demo/src/main/.classpath b/demo/src/main/.classpath index 846a9fbecc..3ae82311ba 100644 --- a/demo/src/main/.classpath +++ b/demo/src/main/.classpath @@ -4,7 +4,7 @@ - + diff --git a/library/src/main/.project b/library/src/main/.project index 5d04c5fa5c..21ae416a8b 100644 --- a/library/src/main/.project +++ b/library/src/main/.project @@ -30,24 +30,4 @@ com.android.ide.eclipse.adt.AndroidNature org.eclipse.jdt.core.javanature - - - 1363908161147 - - 22 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-false-false-BUILD - - - - 1363908161148 - - 10 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-true-false-build - - - From 78f34cf480516bf1d63279e63a55a49591af7e6c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 18:22:26 +0000 Subject: [PATCH 057/110] Add svg source for diagrams. --- library/doc_src/images/exoplayer_diagrams.svg | 2126 +++++++++++++++++ 1 file changed, 2126 insertions(+) create mode 100644 library/doc_src/images/exoplayer_diagrams.svg diff --git a/library/doc_src/images/exoplayer_diagrams.svg b/library/doc_src/images/exoplayer_diagrams.svg new file mode 100644 index 0000000000..6a703263a9 --- /dev/null +++ b/library/doc_src/images/exoplayer_diagrams.svg @@ -0,0 +1,2126 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From da26b03d9cf4e63fed9bf3f6d0115159c6b16fb0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 3 Nov 2014 12:01:25 +0000 Subject: [PATCH 058/110] Minor setup tweaks. --- CONTRIBUTING.md | 2 +- README.md | 2 +- settings.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09cc6bdf41..12b5bcf219 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# How to contribute # +# How to Contribute # We'd love to hear your feedback. Please open new issues describing any bugs, feature requests or suggestions that you have. diff --git a/README.md b/README.md index 6faf3a264b..3a0e309a7f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both -locally and over the internet. ExoPlayer supports features not currently +locally and over the Internet. ExoPlayer supports features not currently supported by Android’s MediaPlayer API (as of KitKat), including DASH and SmoothStreaming adaptive playbacks, persistent caching and custom renderers. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and diff --git a/settings.gradle b/settings.gradle index 6a19aced48..63dd803377 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. include ':library' -include ':demo' \ No newline at end of file +include ':demo' From 19eb7795fe27d78193808a0cccdfa6bdc0b540df Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 3 Nov 2014 12:01:53 +0000 Subject: [PATCH 059/110] Fix default startNumber. Issue: #108 --- .../dash/mpd/MediaPresentationDescriptionParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 de5fc9bdb6..bf1ba532b7 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 @@ -362,7 +362,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", parent != null ? parent.presentationTimeOffset : 0); long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1); - int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0); + int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 1); RangedUri initialization = null; List timeline = null; @@ -406,7 +406,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", parent != null ? parent.presentationTimeOffset : 0); long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1); - int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0); + int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 1); UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", parent != null ? parent.mediaTemplate : null); UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization", From deb7f2badd8719ea9cf55038e325d1dc75c482b9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 3 Nov 2014 12:06:35 +0000 Subject: [PATCH 060/110] Add AAC test stream. --- .../main/java/com/google/android/exoplayer/demo/Samples.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 93d08af4cc..27d71a7d55 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -136,6 +136,9 @@ package com.google.android.exoplayer.demo; DemoUtil.TYPE_OTHER, false, true), new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj", DemoUtil.TYPE_OTHER, false, true), + new Sample("Apple AAC 10s", "uid:misc:appleaacseg", "https://devimages.apple.com.edgekey.net/" + + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", + DemoUtil.TYPE_OTHER, false, true), }; private Samples() {} From dedbd5367f35ce70ace99f77c314653392de9231 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 3 Nov 2014 15:54:58 +0000 Subject: [PATCH 061/110] Use largeHeap in demo app --- demo/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 8812dbc014..a9385058aa 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -29,7 +29,8 @@ + android:largeHeap="true" + android:allowBackup="false"> Date: Tue, 4 Nov 2014 14:06:37 -0500 Subject: [PATCH 062/110] Add cookies support and use the same UserAgent in ManifestFetcher and in HttpDataSource. --- .../google/android/exoplayer/demo/DemoUtil.java | 17 +++++++++++++++++ .../exoplayer/demo/full/FullPlayerActivity.java | 2 ++ .../full/player/DashVodRendererBuilder.java | 2 +- .../player/SmoothStreamingRendererBuilder.java | 2 +- .../demo/simple/DashVodRendererBuilder.java | 2 +- .../demo/simple/SimplePlayerActivity.java | 2 ++ .../simple/SmoothStreamingRendererBuilder.java | 2 +- .../android/exoplayer/util/ManifestFetcher.java | 17 ++++++++++++++--- 8 files changed, 39 insertions(+), 7 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index 6479b28a7e..880ecc3286 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -28,6 +28,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -50,6 +53,13 @@ public class DemoUtil { public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; + private static final CookieManager defaultCookieManager; + + static { + defaultCookieManager = new CookieManager(); + defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + } + public static String getUserAgent(Context context) { String versionName; try { @@ -105,4 +115,11 @@ public class DemoUtil { return bytes; } + public static void setDefaultCookieManager() { + CookieHandler currentHandler = CookieHandler.getDefault(); + if (currentHandler != defaultCookieManager) { + CookieHandler.setDefault(defaultCookieManager); + } + } + } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 9966124ced..c1fe79eb0f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -127,6 +127,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba videoButton = (Button) findViewById(R.id.video_controls); audioButton = (Button) findViewById(R.id.audio_controls); textButton = (Button) findViewById(R.id.text_controls); + + DemoUtil.setDefaultCookieManager(); } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index d6dff9e60e..4c6e0f73bc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -97,7 +97,7 @@ public class DashVodRendererBuilder implements RendererBuilder, this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url); + new ManifestFetcher(parser, contentId, url, userAgent); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 9922bab839..bc16d192a6 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -92,7 +92,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); manifestFetcher = new ManifestFetcher(parser, contentId, - url + "/Manifest"); + url + "/Manifest", userAgent); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index fda21da54f..547ca0fefc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -77,7 +77,7 @@ import java.util.ArrayList; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url); + new ManifestFetcher(parser, contentId, url, userAgent); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index 73d2605c94..1622998ae4 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -113,6 +113,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call shutterView = findViewById(R.id.shutter); surfaceView = (VideoSurfaceView) findViewById(R.id.surface_view); surfaceView.getHolder().addCallback(this); + + DemoUtil.setDefaultCookieManager(); } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index 4d92c0458c..8686fa3b5a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -77,7 +77,7 @@ import java.util.ArrayList; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); manifestFetcher = new ManifestFetcher(parser, contentId, - url + "/Manifest"); + url + "/Manifest", userAgent); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index c72a856919..1facaa4774 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -63,6 +63,7 @@ public class ManifestFetcher implements Loader.Callback { /* package */ final ManifestParser parser; /* package */ final String manifestUrl; /* package */ final String contentId; + /* package */ final String userAgent; private int enabledCount; private Loader loader; @@ -79,11 +80,14 @@ public class ManifestFetcher implements Loader.Callback { * @param parser A parser to parse the loaded manifest data. * @param contentId The content id of the content being loaded. May be null. * @param manifestUrl The manifest location. + * @param userAgent The User-Agent string that should be used. */ - public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl) { + public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl, + String userAgent) { this.parser = parser; this.contentId = contentId; this.manifestUrl = manifestUrl; + this.userAgent = userAgent; } /** @@ -167,7 +171,7 @@ public class ManifestFetcher implements Loader.Callback { loader = new Loader("manifestLoader"); } if (!loader.isLoading()) { - currentLoadable = new ManifestLoadable(); + currentLoadable = new ManifestLoadable(userAgent); loader.startLoading(currentLoadable, this); } } @@ -217,7 +221,7 @@ public class ManifestFetcher implements Loader.Callback { this.callbackLooper = callbackLooper; this.wrappedCallback = wrappedCallback; singleUseLoader = new Loader("manifestLoader:single"); - singleUseLoadable = new ManifestLoadable(); + singleUseLoadable = new ManifestLoadable(userAgent); } public void startLoading() { @@ -265,9 +269,15 @@ public class ManifestFetcher implements Loader.Callback { private static final int TIMEOUT_MILLIS = 10000; + private final String userAgent; + /* package */ volatile T result; private volatile boolean isCanceled; + public ManifestLoadable(String userAgent) { + this.userAgent = userAgent; + } + @Override public void cancelLoad() { // We don't actually cancel anything, but we need to record the cancellation so that @@ -302,6 +312,7 @@ public class ManifestFetcher implements Loader.Callback { connection.setConnectTimeout(TIMEOUT_MILLIS); connection.setReadTimeout(TIMEOUT_MILLIS); connection.setDoOutput(false); + connection.setRequestProperty("User-Agent", userAgent); connection.connect(); return connection; } From d2e73dd566175c57e7afa481987a5be8fdd025eb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 6 Nov 2014 19:26:41 +0000 Subject: [PATCH 063/110] Add brackets to make expression clearer. --- .../com/google/android/exoplayer/MediaCodecTrackRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7904977dee..c629e365ee 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -673,7 +673,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected boolean isReady() { return format != null && !waitingForKeys - && sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod(); + && (sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod()); } /** From eccf8d792433d8048f79b0a79ac7659307fc330c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 6 Nov 2014 19:27:28 +0000 Subject: [PATCH 064/110] Minor Webvtt parsing tweaks --- .../exoplayer/text/webvtt/WebvttParser.java | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java index d3a560c2d9..baace215f5 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java @@ -53,8 +53,13 @@ public class WebvttParser implements SubtitleParser { private static final long SAMPLING_RATE = 90; + private static final String WEBVTT_METADATA_HEADER_STRING = "\\S*[:=]\\S*"; + private static final Pattern WEBVTT_METADATA_HEADER = + Pattern.compile(WEBVTT_METADATA_HEADER_STRING); + private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}"; private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING); + private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+"); private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+"); @@ -90,30 +95,33 @@ public class WebvttParser implements SubtitleParser { throw new ParserException("Expected WEBVTT. Got " + line); } - // after "WEBVTT" there should be either an empty line or an "X-TIMESTAMP-MAP" line and then - // and empty line - line = webvttData.readLine(); - if (!line.isEmpty()) { - if (!line.startsWith("X-TIMESTAMP-MAP")) { - throw new ParserException("Expected an empty line or X-TIMESTAMP-MAP. Got " + line); - } - - // parse the media timestamp - Matcher matcher = MEDIA_TIMESTAMP.matcher(line); - if (!matcher.find()) { - throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestmap: " + line); - } else { - mediaTimestampUs = (Long.parseLong(matcher.group().substring(7)) * 1000) / SAMPLING_RATE - - mediaTimestampOffsetUs; - } - mediaTimestampUs = getAdjustedStartTime(mediaTimestampUs); - - // read in the next line (which should be an empty line) + // parse the remainder of the header + while (true) { line = webvttData.readLine(); - } - if (!line.isEmpty()) { - throw new ParserException("Expected an empty line after WEBVTT or X-TIMESTAMP-MAP. Got " - + line); + if (line == null) { + // we reached EOF before finishing the header + throw new ParserException("Expected an empty line after webvtt header"); + } else if (line.isEmpty()) { + // we've read the newline that separates the header from the body + break; + } + + Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line); + if (!matcher.find()) { + throw new ParserException("Expected webvtt metadata header; got: " + line); + } + + if (line.startsWith("X-TIMESTAMP-MAP")) { + // parse the media timestamp + Matcher timestampMatcher = MEDIA_TIMESTAMP.matcher(line); + if (!timestampMatcher.find()) { + throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); + } else { + mediaTimestampUs = (Long.parseLong(timestampMatcher.group().substring(7)) * 1000) + / SAMPLING_RATE - mediaTimestampOffsetUs; + } + mediaTimestampUs = getAdjustedStartTime(mediaTimestampUs); + } } // process the cues and text From 1653e81687933735b3c192a5ebdcf1de79295a88 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 6 Nov 2014 19:28:21 +0000 Subject: [PATCH 065/110] Add configurable retry count to ChunkSampleSource --- .../exoplayer/chunk/ChunkSampleSource.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 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 f7d556f3c8..263a47a5a5 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 @@ -140,6 +140,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private static final int NO_RESET_PENDING = -1; + private static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1; + private final int eventSourceId; private final LoadControl loadControl; private final ChunkSource chunkSource; @@ -150,6 +152,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private final boolean frameAccurateSeeking; private final Handler eventHandler; private final EventListener eventListener; + private final int minLoadableRetryCount; private int state; private long downstreamPositionUs; @@ -175,6 +178,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler, EventListener eventListener, int eventSourceId) { + this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, eventHandler, + eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); + } + + public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler, + EventListener eventListener, int eventSourceId, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.loadControl = loadControl; this.bufferSizeContribution = bufferSizeContribution; @@ -182,6 +192,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; + this.minLoadableRetryCount = minLoadableRetryCount; currentLoadableHolder = new ChunkOperationHolder(); mediaChunks = new LinkedList(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); @@ -287,9 +298,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { downstreamPositionUs = positionUs; if (isPendingReset()) { - if (currentLoadableException != null) { - throw currentLoadableException; - } + maybeThrowLoadableException(); IOException chunkSourceException = chunkSource.getError(); if (chunkSourceException != null) { throw chunkSourceException; @@ -342,9 +351,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { onSampleRead(mediaChunk, sampleHolder); return SAMPLE_READ; } else { - if (currentLoadableException != null) { - throw currentLoadableException; - } + maybeThrowLoadableException(); return NOTHING_READ; } } @@ -369,6 +376,12 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } } + private void maybeThrowLoadableException() throws IOException { + if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + throw currentLoadableException; + } + } + private MediaChunk getMediaChunk(long positionUs) { Iterator mediaChunkIterator = mediaChunks.iterator(); while (mediaChunkIterator.hasNext()) { From 4460b7c62605ce1c938878698b98c976f5f5156e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:06:49 +0000 Subject: [PATCH 066/110] Fix typo --- .../com/google/android/exoplayer/text/CaptionStyleCompat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java index 3e406b5854..61aca14bba 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java +++ b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java @@ -113,7 +113,7 @@ public final class CaptionStyleCompat { if (Util.SDK_INT >= 21) { return createFromCaptionStyleV21(captionStyle); } else { - // Note - Any caller must be on at least API level 19 of greater (because CaptionStyle did + // Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did // not exist in earlier API levels). return createFromCaptionStyleV19(captionStyle); } From bc871c94a6490b0b5577ec377a291afdd4dd8ff7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:08:16 +0000 Subject: [PATCH 067/110] Add bitrate to MediaFormat --- .../google/android/exoplayer/MediaFormat.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 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 24af7ba1a0..24db47ff77 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -46,6 +46,8 @@ public class MediaFormat { public final int channelCount; public final int sampleRate; + public final int bitrate; + private int maxWidth; private int maxHeight; @@ -69,13 +71,19 @@ public class MediaFormat { public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, int height, float pixelWidthHeightRatio, List initializationData) { return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE, - NO_VALUE, initializationData); + NO_VALUE, NO_VALUE, initializationData); } public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, int sampleRate, List initializationData) { return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, - sampleRate, initializationData); + sampleRate, NO_VALUE, initializationData); + } + + public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, + int sampleRate, int bitrate, List initializationData) { + return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, + sampleRate, bitrate, initializationData); } @TargetApi(16) @@ -87,6 +95,7 @@ public class MediaFormat { height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); + bitrate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_BIT_RATE); pixelWidthHeightRatio = getOptionalFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); initializationData = new ArrayList(); for (int i = 0; format.containsKey("csd-" + i); i++) { @@ -101,7 +110,7 @@ public class MediaFormat { } private MediaFormat(String mimeType, int maxInputSize, int width, int height, - float pixelWidthHeightRatio, int channelCount, int sampleRate, + float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate, List initializationData) { this.mimeType = mimeType; this.maxInputSize = maxInputSize; @@ -110,6 +119,7 @@ public class MediaFormat { this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.channelCount = channelCount; this.sampleRate = sampleRate; + this.bitrate = bitrate; this.initializationData = initializationData == null ? Collections.emptyList() : initializationData; maxWidth = NO_VALUE; @@ -145,6 +155,7 @@ public class MediaFormat { result = 31 * result + maxHeight; result = 31 * result + channelCount; result = 31 * result + sampleRate; + result = 31 * result + bitrate; for (int i = 0; i < initializationData.size(); i++) { result = 31 * result + Arrays.hashCode(initializationData.get(i)); } @@ -180,6 +191,7 @@ public class MediaFormat { || (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight)) || channelCount != other.channelCount || sampleRate != other.sampleRate || !Util.areEqual(mimeType, other.mimeType) + || bitrate != other.bitrate || initializationData.size() != other.initializationData.size()) { return false; } @@ -194,8 +206,8 @@ public class MediaFormat { @Override public String toString() { return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " - + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + maxWidth + ", " - + maxHeight + ")"; + + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", " + + maxWidth + ", " + maxHeight + ")"; } /** @@ -211,6 +223,7 @@ public class MediaFormat { maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_BIT_RATE, bitrate); maybeSetFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio); for (int i = 0; i < initializationData.size(); i++) { format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); From 59688397fa7800005f567faad1e91539ea270bf1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:08:43 +0000 Subject: [PATCH 068/110] Suppress deprecation warnings --- .../main/java/com/google/android/exoplayer/MediaCodecUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 9da62a59d2..670666abc9 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -64,7 +64,10 @@ public class MediaCodecUtil { /** * Returns the best decoder and its capabilities for the given mimeType. If there's no decoder * returns null. + * + * TODO: We need to use the new object based MediaCodecList API. */ + @SuppressWarnings("deprecation") private static synchronized Pair getMediaCodecInfo( String mimeType) { Pair result = codecs.get(mimeType); From f1c646b79374be6e7488208820dfcee378ce79f0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:10:21 +0000 Subject: [PATCH 069/110] Add diagnostic info to decoder exceptions + minor cleanup --- .../MediaCodecAudioTrackRenderer.java | 26 +++++++++---------- .../exoplayer/MediaCodecTrackRenderer.java | 19 +++++++++++++- 2 files changed, 31 insertions(+), 14 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 bcbb39a1fe..dccb962fcd 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -68,8 +68,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { public AudioTrackInitializationException(int audioTrackState, int sampleRate, int channelConfig, int bufferSize) { - super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " + - channelConfig + ", " + bufferSize + ")"); + super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " + + channelConfig + ", " + bufferSize + ")"); this.audioTrackState = audioTrackState; } @@ -538,8 +538,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { // Compute the audio track latency, excluding the latency due to the buffer (leaving // latency due to the mixer and audio hardware driver). audioTrackLatencyUs = - (Integer) audioTrackGetLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - - framesToDurationUs(bufferSize / frameSize); + (Integer) audioTrackGetLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L + - framesToDurationUs(bufferSize / frameSize); // Sanity check that the latency is non-negative. audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0); // Sanity check that the latency isn't too large. @@ -612,19 +612,19 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { if (temporaryBufferSize == 0) { // This is the first time we've seen this {@code buffer}. // Note: presentationTimeUs corresponds to the end of the sample, not the start. - long bufferStartTime = bufferInfo.presentationTimeUs - - framesToDurationUs(bufferInfo.size / frameSize); + long bufferStartTime = bufferInfo.presentationTimeUs + - framesToDurationUs(bufferInfo.size / frameSize); if (audioTrackStartMediaTimeState == START_NOT_SET) { audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime); audioTrackStartMediaTimeState = START_IN_SYNC; } else { // Sanity check that bufferStartTime is consistent with the expected value. - long expectedBufferStartTime = audioTrackStartMediaTimeUs + - framesToDurationUs(submittedBytes / frameSize); + long expectedBufferStartTime = audioTrackStartMediaTimeUs + + framesToDurationUs(submittedBytes / frameSize); if (audioTrackStartMediaTimeState == START_IN_SYNC && Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { - Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + - bufferStartTime + "]"); + Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + + bufferStartTime + "]"); audioTrackStartMediaTimeState = START_NEED_SYNC; } if (audioTrackStartMediaTimeState == START_NEED_SYNC) { @@ -679,7 +679,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @TargetApi(21) - private int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { + private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { return audioTrack.write(buffer, size, AudioTrack.WRITE_NON_BLOCKING); } @@ -703,8 +703,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } private int getPendingFrameCount() { - return audioTrack == null ? - 0 : (int) (submittedBytes / frameSize - getPlaybackHeadPosition()); + return audioTrack == null + ? 0 : (int) (submittedBytes / frameSize - getPlaybackHeadPosition()); } @Override 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 c629e365ee..1fa103f395 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaCodec; +import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CryptoException; import android.media.MediaCrypto; import android.media.MediaExtractor; @@ -70,10 +71,24 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { */ public final String decoderName; + /** + * An optional developer-readable diagnostic information string. May be null. + */ + public final String diagnosticInfo; + public DecoderInitializationException(String decoderName, MediaFormat mediaFormat, - Exception cause) { + Throwable cause) { super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause); this.decoderName = decoderName; + this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; + } + + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; } } @@ -235,6 +250,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { codec.configure(x, null, crypto, 0); } + @SuppressWarnings("deprecation") protected final void maybeInitCodec() throws ExoPlaybackException { if (!shouldInitCodec()) { return; @@ -694,6 +710,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @return True if it may be possible to drain more output data. False otherwise. * @throws ExoPlaybackException If an error occurs draining the output buffer. */ + @SuppressWarnings("deprecation") private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { From 2d97d31a9e549eb73b4106ae1f981a9704651a8e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:11:52 +0000 Subject: [PATCH 070/110] Add ability to make fine-grained frame release timestamp adjustments --- .../full/player/DashVodRendererBuilder.java | 2 +- .../full/player/DefaultRendererBuilder.java | 2 +- .../SmoothStreamingRendererBuilder.java | 2 +- .../MediaCodecVideoTrackRenderer.java | 73 ++++++++++++++++--- 4 files changed, 67 insertions(+), 12 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 4c6e0f73bc..e49de71d7e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -176,7 +176,7 @@ public class DashVodRendererBuilder implements RendererBuilder, DemoPlayer.TYPE_VIDEO); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - mainHandler, player, 50); + null, mainHandler, player, 50); // Build the audio renderer. final String[] audioTrackNames; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java index 1afca8f54f..3a5b5ce036 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java @@ -49,7 +49,7 @@ public class DefaultRendererBuilder implements RendererBuilder { FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - player.getMainHandler(), player, 50); + null, player.getMainHandler(), player, 50); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, null, true, player.getMainHandler(), player); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index bc16d192a6..ada15b97aa 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -164,7 +164,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, DemoPlayer.TYPE_VIDEO); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - mainHandler, player, 50); + null, mainHandler, player, 50); // Build the audio renderer. final String[] audioTrackNames; 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 5f912ee52e..858597e3ac 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -75,6 +75,34 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } + /** + * An interface for fine-grained adjustment of frame release times. + */ + public interface FrameReleaseTimeHelper { + + /** + * Enables the helper. + */ + void enable(); + + /** + * Disables the helper. + */ + void disable(); + + /** + * Called to make a fine-grained adjustment to a frame release time. + * + * @param framePresentationTimeUs The frame's media presentation time, in microseconds. + * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in + * the same time base as {@link System#nanoTime()}. + * @return An adjusted release time for the frame, in nanoseconds and in the same time base as + * {@link System#nanoTime()}. + */ + public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs); + + } + // TODO: Use MediaFormat constants if these get exposed through the API. See [redacted]. private static final String KEY_CROP_LEFT = "crop-left"; private static final String KEY_CROP_RIGHT = "crop-right"; @@ -88,6 +116,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { */ public static final int MSG_SET_SURFACE = 1; + private final FrameReleaseTimeHelper frameReleaseTimeHelper; private final EventListener eventListener; private final long allowedJoiningTimeUs; private final int videoScalingMode; @@ -162,7 +191,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs) { this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, - allowedJoiningTimeMs, null, null, -1); + allowedJoiningTimeMs, null, null, null, -1); } /** @@ -180,8 +209,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { - this(source, null, true, videoScalingMode, allowedJoiningTimeMs, eventHandler, eventListener, - maxDroppedFrameCountToNotify); + this(source, null, true, videoScalingMode, allowedJoiningTimeMs, null, eventHandler, + eventListener, maxDroppedFrameCountToNotify); } /** @@ -197,6 +226,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * {@link MediaCodec#setVideoScalingMode(int)}. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. + * @param frameReleaseTimeHelper An optional helper to make fine-grained adjustments to frame + * release times. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -205,10 +236,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { */ public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs, - Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { + FrameReleaseTimeHelper frameReleaseTimeHelper, Handler eventHandler, + EventListener eventListener, int maxDroppedFrameCountToNotify) { super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); this.videoScalingMode = videoScalingMode; this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000; + this.frameReleaseTimeHelper = frameReleaseTimeHelper; this.eventListener = eventListener; this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify; joiningDeadlineUs = -1; @@ -232,6 +265,9 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { if (joining && allowedJoiningTimeUs > 0) { joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs; } + if (frameReleaseTimeHelper != null) { + frameReleaseTimeHelper.enable(); + } } @Override @@ -283,6 +319,9 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { lastReportedWidth = -1; lastReportedHeight = -1; lastReportedPixelWidthHeightRatio = -1; + if (frameReleaseTimeHelper != null) { + frameReleaseTimeHelper.disable(); + } super.onDisabled(); } @@ -362,8 +401,24 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { return true; } - long elapsedSinceStartOfLoop = SystemClock.elapsedRealtime() * 1000 - elapsedRealtimeUs; - long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoop; + // Compute how many microseconds it is until the buffer's presentation time. + long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; + long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs; + + // Compute the buffer's desired release time in nanoseconds. + long systemTimeNs = System.nanoTime(); + long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); + + // Apply a timestamp adjustment, if there is one. + long adjustedReleaseTimeNs; + if (frameReleaseTimeHelper != null) { + adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( + bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs); + earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; + } else { + adjustedReleaseTimeNs = unadjustedFrameReleaseTimeNs; + } + if (earlyUs < -30000) { // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex); @@ -383,7 +438,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { if (Util.SDK_INT >= 21) { // Let the underlying framework time the release. if (earlyUs < 50000) { - renderOutputBufferTimedV21(codec, bufferIndex, System.nanoTime() + (earlyUs * 1000L)); + renderOutputBufferTimedV21(codec, bufferIndex, adjustedReleaseTimeNs); return true; } } else { @@ -436,10 +491,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @TargetApi(21) - private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long nanoTime) { + private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBufferTimed"); - codec.releaseOutputBuffer(bufferIndex, nanoTime); + codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); TraceUtil.endSection(); codecCounters.renderedOutputBufferCount++; maybeNotifyDrawnToSurface(); From 456d53e178e8535e99dc58a117d3e86e598ac22d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:13:02 +0000 Subject: [PATCH 071/110] Minor cleanup. --- .../google/android/exoplayer/util/ManifestFetcher.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 1facaa4774..1c2c4cdcbf 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -171,7 +171,7 @@ public class ManifestFetcher implements Loader.Callback { loader = new Loader("manifestLoader"); } if (!loader.isLoading()) { - currentLoadable = new ManifestLoadable(userAgent); + currentLoadable = new ManifestLoadable(); loader.startLoading(currentLoadable, this); } } @@ -221,7 +221,7 @@ public class ManifestFetcher implements Loader.Callback { this.callbackLooper = callbackLooper; this.wrappedCallback = wrappedCallback; singleUseLoader = new Loader("manifestLoader:single"); - singleUseLoadable = new ManifestLoadable(userAgent); + singleUseLoadable = new ManifestLoadable(); } public void startLoading() { @@ -269,15 +269,9 @@ public class ManifestFetcher implements Loader.Callback { private static final int TIMEOUT_MILLIS = 10000; - private final String userAgent; - /* package */ volatile T result; private volatile boolean isCanceled; - public ManifestLoadable(String userAgent) { - this.userAgent = userAgent; - } - @Override public void cancelLoad() { // We don't actually cancel anything, but we need to record the cancellation so that From d14e11c507f2d11b934b6a6e97aff7558a2eaacb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:13:55 +0000 Subject: [PATCH 072/110] Additional extraction for AC3 --- .../android/exoplayer/parser/mp4/Atom.java | 4 + .../parser/mp4/FragmentedMp4Extractor.java | 133 +++++++++++++++--- 2 files changed, 117 insertions(+), 20 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 32eb1937ae..60c9ae6984 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 @@ -24,6 +24,10 @@ import java.util.ArrayList; public static final int TYPE_esds = 0x65736473; public static final int TYPE_mdat = 0x6D646174; public static final int TYPE_mp4a = 0x6D703461; + public static final int TYPE_ac_3 = 0x61632D33; // ac-3 + public static final int TYPE_dac3 = 0x64616333; + public static final int TYPE_ec_3 = 0x65632D33; // ec-3 + public static final int TYPE_dec3 = 0x64656333; public static final int TYPE_tfdt = 0x74666474; public static final int TYPE_tfhd = 0x74666864; public static final int TYPE_trex = 0x74726578; 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 e6bf68f30a..1229e87d5b 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 @@ -65,6 +65,11 @@ public final class FragmentedMp4Extractor implements Extractor { 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}; + /** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */ + private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; + /** Nominal bit-rates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ + private static final int[] AC3_BIT_RATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, + 192, 224, 256, 320, 384, 448, 512, 576, 640}; // Parser states private static final int STATE_READING_ATOM_HEADER = 0; @@ -512,11 +517,12 @@ public final class FragmentedMp4Extractor implements Extractor { parseAvcFromParent(stsd, childStartPosition, childAtomSize); mediaFormat = avc.first; trackEncryptionBoxes[i] = avc.second; - } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) { - Pair mp4a = - parseMp4aFromParent(stsd, childStartPosition, childAtomSize); - mediaFormat = mp4a.first; - trackEncryptionBoxes[i] = mp4a.second; + } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca + || childAtomType == Atom.TYPE_ac_3) { + Pair audioSampleEntry = + parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize); + mediaFormat = audioSampleEntry.first; + trackEncryptionBoxes[i] = audioSampleEntry.second; } stsd.setPosition(childStartPosition + childAtomSize); } @@ -556,15 +562,15 @@ public final class FragmentedMp4Extractor implements Extractor { return Pair.create(format, trackEncryptionBox); } - private static Pair parseMp4aFromParent(ParsableByteArray parent, - int position, int size) { + private static Pair parseAudioSampleEntry( + ParsableByteArray parent, int atomType, int position, int size) { parent.setPosition(position + ATOM_HEADER_SIZE); - // Start of the mp4a atom (defined in 14496-14) parent.skip(16); int channelCount = parent.readUnsignedShort(); int sampleSize = parent.readUnsignedShort(); parent.skip(4); int sampleRate = parent.readUnsignedFixedPoint1616(); + int bitrate = MediaFormat.NO_VALUE; byte[] initializationData = null; TrackEncryptionBox trackEncryptionBox = null; @@ -574,25 +580,97 @@ public final class FragmentedMp4Extractor implements Extractor { int childStartPosition = parent.getPosition(); int childAtomSize = parent.readInt(); int childAtomType = parent.readInt(); - if (childAtomType == Atom.TYPE_esds) { - initializationData = parseEsdsFromParent(parent, childStartPosition); - // TODO: Do we really need to do this? See [redacted] - // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. - Pair audioSpecificConfig = - CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); - sampleRate = audioSpecificConfig.first; - channelCount = audioSpecificConfig.second; - } else if (childAtomType == Atom.TYPE_sinf) { - trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); + if (atomType == Atom.TYPE_mp4a || atomType == Atom.TYPE_enca) { + if (childAtomType == Atom.TYPE_esds) { + initializationData = parseEsdsFromParent(parent, childStartPosition); + // TODO: Do we really need to do this? See [redacted] + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. + Pair audioSpecificConfig = + CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } else if (childAtomType == Atom.TYPE_sinf) { + trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); + } + } else if (atomType == Atom.TYPE_ac_3 && childAtomType == Atom.TYPE_dac3) { + // TODO: Choose the right AC-3 track based on the contents of dac3/dec3. + Ac3Format ac3Format = + parseAc3SpecificBoxFromParent(parent, childStartPosition); + if (ac3Format != null) { + sampleRate = ac3Format.sampleRate; + channelCount = ac3Format.channelCount; + bitrate = ac3Format.bitrate; + } + + // TODO: Add support for encrypted AC-3. + trackEncryptionBox = null; + } else if (atomType == Atom.TYPE_ec_3 && childAtomType == Atom.TYPE_dec3) { + sampleRate = parseEc3SpecificBoxFromParent(parent, childStartPosition); + trackEncryptionBox = null; } childPosition += childAtomSize; } - MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", sampleSize, channelCount, - sampleRate, Collections.singletonList(initializationData)); + String mimeType; + if (atomType == Atom.TYPE_ac_3) { + mimeType = MimeTypes.AUDIO_AC3; + } else if (atomType == Atom.TYPE_ec_3) { + mimeType = MimeTypes.AUDIO_EC3; + } else { + mimeType = MimeTypes.AUDIO_AAC; + } + + MediaFormat format = MediaFormat.createAudioFormat( + mimeType, sampleSize, channelCount, sampleRate, bitrate, + initializationData == null ? null : Collections.singletonList(initializationData)); return Pair.create(format, trackEncryptionBox); } + private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) { + // Start of the dac3 atom (defined in ETSI TS 102 366) + parent.setPosition(position + ATOM_HEADER_SIZE); + + // fscod (sample rate code) + int fscod = (parent.readUnsignedByte() & 0xC0) >> 6; + int sampleRate; + switch (fscod) { + case 0: + sampleRate = 48000; + break; + case 1: + sampleRate = 44100; + break; + case 2: + sampleRate = 32000; + break; + default: + // TODO: The decoder should not use this stream. + return null; + } + + int nextByte = parent.readUnsignedByte(); + + // Map acmod (audio coding mode) onto a channel count. + int channelCount = AC3_CHANNEL_COUNTS[(nextByte & 0x38) >> 3]; + + // lfeon (low frequency effects on) + if ((nextByte & 0x04) != 0) { + channelCount++; + } + + // Map bit_rate_code onto a bit-rate in kbit/s. + int bitrate = AC3_BIT_RATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; + + return new Ac3Format(channelCount, sampleRate, bitrate); + } + + private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) { + // Start of the dec3 atom (defined in ETSI TS 102 366) + parent.setPosition(position + ATOM_HEADER_SIZE); + // TODO: Implement parsing for enhanced AC-3 with multiple sub-streams. + return 0; + } + private static List parseAvcCFromParent(ParsableByteArray parent, int position) { parent.setPosition(position + ATOM_HEADER_SIZE + 4); // Start of the AVCDecoderConfigurationRecord (defined in 14496-15) @@ -1182,4 +1260,19 @@ public final class FragmentedMp4Extractor implements Extractor { return result; } + /** Represents the format for AC-3 audio. */ + private static final class Ac3Format { + + public final int channelCount; + public final int sampleRate; + public final int bitrate; + + public Ac3Format(int channelCount, int sampleRate, int bitrate) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.bitrate = bitrate; + } + + } + } From cb0684597609a8a158e6a5792ea644e868eb68ec Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:16:02 +0000 Subject: [PATCH 073/110] Minor linebreak fixes --- .../exoplayer/demo/full/player/DashVodRendererBuilder.java | 6 +++--- .../exoplayer/demo/full/player/DefaultRendererBuilder.java | 4 ++-- .../demo/full/player/SmoothStreamingRendererBuilder.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index e49de71d7e..9ce6613fa2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -175,13 +175,13 @@ public class DashVodRendererBuilder implements RendererBuilder, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - null, mainHandler, player, 50); + drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, + mainHandler, player, 50); // Build the audio renderer. final String[] audioTrackNames; final MultiTrackChunkSource audioChunkSource; - final MediaCodecAudioTrackRenderer audioRenderer; + final TrackRenderer audioRenderer; if (audioRepresentationsList.isEmpty()) { audioTrackNames = null; audioChunkSource = null; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java index 3a5b5ce036..4aea8e642e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java @@ -48,8 +48,8 @@ public class DefaultRendererBuilder implements RendererBuilder { // Build the video and audio renderers. FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, - null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - null, player.getMainHandler(), player, 50); + null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(), + player, 50); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, null, true, player.getMainHandler(), player); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index ada15b97aa..2fb473239f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -163,8 +163,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - null, mainHandler, player, 50); + drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, + mainHandler, player, 50); // Build the audio renderer. final String[] audioTrackNames; From 685e1d1f06507771de29807c8874d3664316a9d8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Nov 2014 18:54:45 +0000 Subject: [PATCH 074/110] Minimize memory leak risks. Remove implicit back-reference from playback thread to player. --- .../android/exoplayer/ExoPlayerImplInternal.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) 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 2dfac29519..e72734e80a 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -17,9 +17,9 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.PriorityHandlerThread; import com.google.android.exoplayer.util.TraceUtil; -import android.annotation.SuppressLint; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -83,7 +83,6 @@ import java.util.List; private volatile long positionUs; private volatile long bufferedPositionUs; - @SuppressLint("HandlerLeak") public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady, boolean[] rendererEnabledFlags, int minBufferMs, int minRebufferMs) { this.eventHandler = eventHandler; @@ -101,15 +100,10 @@ import java.util.List; mediaClock = new MediaClock(); enabledRenderers = new ArrayList(rendererEnabledFlags.length); - internalPlaybackThread = new HandlerThread(getClass().getSimpleName() + ":Handler") { - @Override - public void run() { - // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can - // not normally change to this priority" is incorrect. - Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO); - super.run(); - } - }; + // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can + // not normally change to this priority" is incorrect. + internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler", + Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = new Handler(internalPlaybackThread.getLooper(), this); } From 6a544da2f8266e13173a4643b8fc8c9d22d2ba4e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Nov 2014 18:57:07 +0000 Subject: [PATCH 075/110] Use new MediaCodecList APIs on L. --- .../google/android/exoplayer/DecoderInfo.java | 2 +- .../exoplayer/MediaCodecTrackRenderer.java | 13 +- .../android/exoplayer/MediaCodecUtil.java | 207 +++++++++++++++--- 3 files changed, 176 insertions(+), 46 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java b/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java index 8b2a0dd4a8..440148a556 100644 --- a/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java @@ -29,7 +29,7 @@ public final class DecoderInfo { public final String name; /** - * Whether the decoder is adaptive. + * Whether the decoder supports seamless resolution switches. * * @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String) * @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback 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 1fa103f395..fdd4020769 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -280,11 +280,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } } - DecoderInfo selectedDecoderInfo = MediaCodecUtil.getDecoderInfo(mimeType); + DecoderInfo selectedDecoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, + requiresSecureDecoder); String selectedDecoderName = selectedDecoderInfo.name; - if (requiresSecureDecoder) { - selectedDecoderName = getSecureDecoderName(selectedDecoderName); - } codecIsAdaptive = selectedDecoderInfo.adaptive; try { codec = MediaCodec.createByCodecName(selectedDecoderName); @@ -765,13 +763,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) throws ExoPlaybackException; - /** - * Returns the name of the secure variant of a given decoder. - */ - private static String getSecureDecoderName(String rawDecoderName) { - return rawDecoderName + ".secure"; - } - private void notifyDecoderInitializationError(final DecoderInitializationException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 670666abc9..3dfe5f1afe 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -23,6 +23,7 @@ import android.media.MediaCodecInfo; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; +import android.text.TextUtils; import android.util.Pair; import java.util.HashMap; @@ -33,60 +34,79 @@ import java.util.HashMap; @TargetApi(16) public class MediaCodecUtil { - private static final HashMap> codecs = - new HashMap>(); + private static final HashMap> codecs = + new HashMap>(); /** - * Get information about the decoder that will be used for a given mime type. If no decoder - * exists for the mime type then null is returned. + * Get information about the decoder that will be used for a given mime type. * * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. * @return Information about the decoder that will be used, or null if no decoder exists. */ - public static DecoderInfo getDecoderInfo(String mimeType) { - Pair info = getMediaCodecInfo(mimeType); + public static DecoderInfo getDecoderInfo(String mimeType, boolean secure) { + Pair info = getMediaCodecInfo(mimeType, secure); if (info == null) { return null; } - return new DecoderInfo(info.first.getName(), isAdaptive(info.second)); + return new DecoderInfo(info.first, isAdaptive(info.second)); } /** - * Optional call to warm the codec cache. Call from any appropriate - * place to hide latency. - */ - public static synchronized void warmCodecs(String[] mimeTypes) { - for (int i = 0; i < mimeTypes.length; i++) { - getMediaCodecInfo(mimeTypes[i]); - } - } - - /** - * Returns the best decoder and its capabilities for the given mimeType. If there's no decoder - * returns null. + * Optional call to warm the codec cache for a given mime type. + *

+ * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}. * - * TODO: We need to use the new object based MediaCodecList API. + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. */ - @SuppressWarnings("deprecation") - private static synchronized Pair getMediaCodecInfo( - String mimeType) { - Pair result = codecs.get(mimeType); - if (result != null) { - return result; + public static synchronized void warmCodec(String mimeType, boolean secure) { + getMediaCodecInfo(mimeType, secure); + } + + /** + * Returns the name of the best decoder and its capabilities for the given mimeType. + */ + private static synchronized Pair getMediaCodecInfo( + String mimeType, boolean secure) { + CodecKey key = new CodecKey(mimeType, secure); + if (codecs.containsKey(key)) { + return codecs.get(key); } - int numberOfCodecs = MediaCodecList.getCodecCount(); + MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 + ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); + int numberOfCodecs = mediaCodecList.getCodecCount(); + boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (int i = 0; i < numberOfCodecs; i++) { - MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); + MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i); String codecName = info.getName(); - if (!info.isEncoder() && codecName.startsWith("OMX.") && !codecName.endsWith(".secure")) { + if (!info.isEncoder() && codecName.startsWith("OMX.") + && (secureDecodersExplicit || !codecName.endsWith(".secure"))) { String[] supportedTypes = info.getSupportedTypes(); for (int j = 0; j < supportedTypes.length; j++) { String supportedType = supportedTypes[j]; if (supportedType.equalsIgnoreCase(mimeType)) { - result = Pair.create(info, info.getCapabilitiesForType(supportedType)); - codecs.put(mimeType, result); - return result; + CodecCapabilities capabilities = info.getCapabilitiesForType(supportedType); + if (!secureDecodersExplicit) { + // Cache variants for secure and insecure playback. Note that the secure decoder is + // inferred, and may not actually exist. + codecs.put(key.secure ? new CodecKey(mimeType, false) : key, + Pair.create(codecName, capabilities)); + codecs.put(key.secure ? key : new CodecKey(mimeType, true), + Pair.create(codecName + ".secure", capabilities)); + } else { + // We can only cache this variant. The other should be listed explicitly. + boolean codecSecure = mediaCodecList.isSecurePlaybackSupported( + info.getCapabilitiesForType(supportedType)); + codecs.put(key.secure == codecSecure ? key : new CodecKey(mimeType, codecSecure), + Pair.create(codecName, capabilities)); + } + if (codecs.containsKey(key)) { + return codecs.get(key); + } } } } @@ -113,7 +133,7 @@ public class MediaCodecUtil { * @return Whether the specified profile is supported at the specified level. */ public static boolean isH264ProfileSupported(int profile, int level) { - Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264); + Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false); if (info == null) { return false; } @@ -133,7 +153,7 @@ public class MediaCodecUtil { * @return the maximum frame size for an H264 stream that can be decoded on the device. */ public static int maxH264DecodableFrameSize() { - Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264); + Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false); if (info == null) { return 0; } @@ -177,4 +197,123 @@ public class MediaCodecUtil { } } + private interface MediaCodecListCompat { + + /** + * The number of codecs in the list. + */ + public int getCodecCount(); + + /** + * The info at the specified index in the list. + * + * @param index The index. + */ + public MediaCodecInfo getCodecInfoAt(int index); + + /** + * @return Returns whether secure decoders are explicitly listed, if present. + */ + public boolean secureDecodersExplicit(); + + /** + * Whether secure playback is supported for the given {@link CodecCapabilities}, which should + * have been obtained from a {@link MediaCodecInfo} obtained from this list. + *

+ * May only be called if {@link #secureDecodersExplicit()} returns true. + */ + public boolean isSecurePlaybackSupported(CodecCapabilities capabilities); + + } + + @TargetApi(21) + private static final class MediaCodecListCompatV21 implements MediaCodecListCompat { + + private final MediaCodecInfo[] mediaCodecInfos; + + public MediaCodecListCompatV21(boolean includeSecure) { + int codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS; + mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); + } + + @Override + public int getCodecCount() { + return mediaCodecInfos.length; + } + + @Override + public MediaCodecInfo getCodecInfoAt(int index) { + return mediaCodecInfos[index]; + } + + @Override + public boolean secureDecodersExplicit() { + return true; + } + + @Override + public boolean isSecurePlaybackSupported(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + } + + } + + @SuppressWarnings("deprecation") + private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { + + @Override + public int getCodecCount() { + return MediaCodecList.getCodecCount(); + } + + @Override + public MediaCodecInfo getCodecInfoAt(int index) { + return MediaCodecList.getCodecInfoAt(index); + } + + @Override + public boolean secureDecodersExplicit() { + return false; + } + + @Override + public boolean isSecurePlaybackSupported(CodecCapabilities capabilities) { + throw new UnsupportedOperationException(); + } + + } + + private static final class CodecKey { + + public final String mimeType; + public final boolean secure; + + public CodecKey(String mimeType, boolean secure) { + this.mimeType = mimeType; + this.secure = secure; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode()); + result = prime * result + (secure ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || obj.getClass() != CodecKey.class) { + return false; + } + CodecKey other = (CodecKey) obj; + return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure; + } + + } + } From 2472637264ecf51bbc3dafcd285d1d2f72715f50 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Nov 2014 19:00:30 +0000 Subject: [PATCH 076/110] Add support for extracting Vorbis audio in WebM Extractor. --- .../exoplayer/dash/DashChunkSource.java | 6 +- .../parser/webm/DefaultEbmlReader.java | 3 +- .../parser/webm/EbmlEventHandler.java | 20 +- .../exoplayer/parser/webm/EbmlReader.java | 4 +- .../exoplayer/parser/webm/WebmExtractor.java | 275 +++++++++++++----- .../android/exoplayer/util/MimeTypes.java | 2 + 6 files changed, 224 insertions(+), 86 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 997da914cb..395d9ba64d 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -87,7 +87,7 @@ public class DashChunkSource implements ChunkSource { formats[i] = representations[i].format; maxWidth = Math.max(formats[i].width, maxWidth); maxHeight = Math.max(formats[i].height, maxHeight); - Extractor extractor = formats[i].mimeType.startsWith(MimeTypes.VIDEO_WEBM) + Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() : new FragmentedMp4Extractor(); extractors.put(formats[i].id, extractor); this.representations.put(formats[i].id, representations[i]); @@ -197,6 +197,10 @@ public class DashChunkSource implements ChunkSource { // Do nothing. } + private boolean mimeTypeIsWebm(String mimeType) { + return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); + } + private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, Representation representation, Extractor extractor, DataSource dataSource, int trigger) { 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 daea459e93..76235fda47 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer.parser.webm; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; @@ -134,7 +135,7 @@ import java.util.Stack; } @Override - public int read(NonBlockingInputStream inputStream) { + public int read(NonBlockingInputStream inputStream) throws ParserException { Assertions.checkState(eventHandler != null); while (true) { while (!masterElementsStack.isEmpty() 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 dcedf9a898..d27cefbc4d 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.parser.webm; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import java.nio.ByteBuffer; @@ -46,41 +47,47 @@ 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 + * @throws ParserException If a parsing error occurs. */ public void onMasterElementStart( - int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes); + int id, long elementOffsetBytes, int headerSizeBytes, + long contentsSizeBytes) throws ParserException; /** * Called when a master element has finished reading in all of its children from the * {@link NonBlockingInputStream}. * * @param id The integer ID of this element + * @throws ParserException If a parsing error occurs. */ - public void onMasterElementEnd(int id); + public void onMasterElementEnd(int id) throws ParserException; /** * 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 + * @throws ParserException If a parsing error occurs. */ - public void onIntegerElement(int id, long value); + public void onIntegerElement(int id, long value) throws ParserException; /** * 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 + * @throws ParserException If a parsing error occurs. */ - public void onFloatElement(int id, double value); + public void onFloatElement(int id, double value) throws ParserException; /** * 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 + * @throws ParserException If a parsing error occurs. */ - public void onStringElement(int id, String value); + public void onStringElement(int id, String value) throws ParserException; /** * Called when a binary element is encountered in the {@link NonBlockingInputStream}. @@ -109,9 +116,10 @@ import java.nio.ByteBuffer; * @param inputStream The {@link NonBlockingInputStream} from which this * element's contents should be read * @return True if the element was read. False otherwise. + * @throws ParserException If a parsing error occurs. */ public boolean onBinaryElement( int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, - NonBlockingInputStream inputStream); + NonBlockingInputStream inputStream) throws ParserException; } 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 dd1c43fce3..955d19f19d 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.parser.webm; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import java.nio.ByteBuffer; @@ -53,8 +54,9 @@ import java.nio.ByteBuffer; * * @param inputStream The input stream from which data should be read * @return One of the {@code RESULT_*} flags defined in this interface + * @throws ParserException If parsing fails. */ - public int read(NonBlockingInputStream inputStream); + public int read(NonBlockingInputStream inputStream) throws ParserException; /** * The total number of bytes consumed by the reader since first created or last {@link #reset()}. 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 7a44d1d960..3402fb26f5 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer.parser.webm; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.parser.Extractor; import com.google.android.exoplayer.parser.SegmentIndex; @@ -27,6 +28,7 @@ import android.annotation.TargetApi; import android.media.MediaExtractor; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import java.util.UUID; @@ -44,6 +46,8 @@ public final class WebmExtractor implements Extractor { private static final String DOC_TYPE_WEBM = "webm"; private static final String CODEC_ID_VP9 = "V_VP9"; + private static final String CODEC_ID_VORBIS = "A_VORBIS"; + private static final int VORBIS_MAX_INPUT_SIZE = 8192; private static final int UNKNOWN = -1; // Element IDs @@ -65,9 +69,13 @@ public final class WebmExtractor implements Extractor { private static final int ID_TRACKS = 0x1654AE6B; private static final int ID_TRACK_ENTRY = 0xAE; private static final int ID_CODEC_ID = 0x86; + private static final int ID_CODEC_PRIVATE = 0x63A2; private static final int ID_VIDEO = 0xE0; private static final int ID_PIXEL_WIDTH = 0xB0; private static final int ID_PIXEL_HEIGHT = 0xBA; + private static final int ID_AUDIO = 0xE1; + private static final int ID_CHANNELS = 0x9F; + private static final int ID_SAMPLING_FREQUENCY = 0xB5; private static final int ID_CUES = 0x1C53BB6B; private static final int ID_CUE_POINT = 0xBB; @@ -96,6 +104,10 @@ public final class WebmExtractor implements Extractor { private long durationUs = UNKNOWN; private int pixelWidth = UNKNOWN; private int pixelHeight = UNKNOWN; + private int channelCount = UNKNOWN; + private int sampleRate = UNKNOWN; + private byte[] codecPrivate; + private boolean seenAudioTrack; private long cuesSizeBytes = UNKNOWN; private long clusterTimecodeUs = UNKNOWN; private long simpleBlockTimecodeUs = UNKNOWN; @@ -114,7 +126,8 @@ public final class WebmExtractor implements Extractor { } @Override - public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { + public int read( + NonBlockingInputStream inputStream, SampleHolder sampleHolder) throws ParserException { this.sampleHolder = sampleHolder; this.readResults = 0; while ((readResults & READ_TERMINATING_RESULTS) == 0) { @@ -176,6 +189,7 @@ public final class WebmExtractor implements Extractor { case ID_CLUSTER: case ID_TRACKS: case ID_TRACK_ENTRY: + case ID_AUDIO: case ID_VIDEO: case ID_CUES: case ID_CUE_POINT: @@ -187,6 +201,7 @@ public final class WebmExtractor implements Extractor { case ID_TIME_CODE: case ID_PIXEL_WIDTH: case ID_PIXEL_HEIGHT: + case ID_CHANNELS: case ID_CUE_TIME: case ID_CUE_CLUSTER_POSITION: return EbmlReader.TYPE_UNSIGNED_INT; @@ -194,8 +209,10 @@ public final class WebmExtractor implements Extractor { case ID_CODEC_ID: return EbmlReader.TYPE_STRING; case ID_SIMPLE_BLOCK: + case ID_CODEC_PRIVATE: return EbmlReader.TYPE_BINARY; case ID_DURATION: + case ID_SAMPLING_FREQUENCY: return EbmlReader.TYPE_FLOAT; default: return EbmlReader.TYPE_UNKNOWN; @@ -203,11 +220,12 @@ public final class WebmExtractor implements Extractor { } /* package */ boolean onMasterElementStart( - int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { + int id, long elementOffsetBytes, int headerSizeBytes, + long contentsSizeBytes) throws ParserException { switch (id) { case ID_SEGMENT: if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) { - throw new IllegalStateException("Multiple Segment elements not supported"); + throw new ParserException("Multiple Segment elements not supported"); } segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes; segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; @@ -223,31 +241,41 @@ public final class WebmExtractor implements Extractor { return true; } - /* package */ boolean onMasterElementEnd(int id) { + /* package */ boolean onMasterElementEnd(int id) throws ParserException { switch (id) { case ID_CUES: buildCues(); return false; case ID_VIDEO: - buildFormat(); + buildVideoFormat(); + return true; + case ID_AUDIO: + seenAudioTrack = true; + return true; + case ID_TRACK_ENTRY: + if (seenAudioTrack) { + // Audio format has to be built here since codec private may not be available at the end + // of ID_AUDIO. + buildAudioFormat(); + } return true; default: return true; } } - /* package */ boolean onIntegerElement(int id, long value) { + /* package */ boolean onIntegerElement(int id, long value) throws ParserException { switch (id) { case ID_EBML_READ_VERSION: // Validate that EBMLReadVersion is supported. This extractor only supports v1. if (value != 1) { - throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported"); + throw new ParserException("EBMLReadVersion " + value + " not supported"); } break; case ID_DOC_TYPE_READ_VERSION: // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. if (value < 1 || value > 2) { - throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported"); + throw new ParserException("DocTypeReadVersion " + value + " not supported"); } break; case ID_TIMECODE_SCALE: @@ -259,6 +287,9 @@ public final class WebmExtractor implements Extractor { case ID_PIXEL_HEIGHT: pixelHeight = (int) value; break; + case ID_CHANNELS: + channelCount = (int) value; + break; case ID_CUE_TIME: cueTimesUs.add(scaleTimecodeToUs(value)); break; @@ -275,24 +306,31 @@ public final class WebmExtractor implements Extractor { } /* package */ boolean onFloatElement(int id, double value) { - if (id == ID_DURATION) { - durationUs = scaleTimecodeToUs((long) value); + switch (id) { + case ID_DURATION: + durationUs = scaleTimecodeToUs((long) value); + break; + case ID_SAMPLING_FREQUENCY: + sampleRate = (int) value; + break; + default: + // pass } return true; } - /* package */ boolean onStringElement(int id, String value) { + /* package */ boolean onStringElement(int id, String value) throws ParserException { switch (id) { case ID_DOC_TYPE: // Validate that DocType is supported. This extractor only supports "webm". if (!DOC_TYPE_WEBM.equals(value)) { - throw new IllegalArgumentException("DocType " + value + " not supported"); + throw new ParserException("DocType " + value + " not supported"); } break; case ID_CODEC_ID: - // Validate that CodecID is supported. This extractor only supports "V_VP9". - if (!CODEC_ID_VP9.equals(value)) { - throw new IllegalArgumentException("CodecID " + value + " not supported"); + // Validate that CodecID is supported. This extractor only supports "V_VP9" and "A_VORBIS". + if (!CODEC_ID_VP9.equals(value) && !CODEC_ID_VORBIS.equals(value)) { + throw new ParserException("CodecID " + value + " not supported"); } break; default: @@ -303,62 +341,70 @@ public final class WebmExtractor implements Extractor { /* package */ boolean onBinaryElement( int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, - NonBlockingInputStream inputStream) { - if (id == ID_SIMPLE_BLOCK) { - // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure - // for info about how data is organized in a SimpleBlock element. + NonBlockingInputStream inputStream) throws ParserException { + switch (id) { + case ID_SIMPLE_BLOCK: + // 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; - } + // 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); + // Value of trackNumber is not used but needs to be read. + reader.readVarint(inputStream); - // Next three bytes have timecode and flags. - reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3); + // Next three bytes have timecode and flags. + reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3); - // First two bytes of the three are the relative timecode. - int timecode = - (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff); - long timecodeUs = scaleTimecodeToUs(timecode); + // First two bytes of the three are the relative timecode. + int timecode = + (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff); + long timecodeUs = scaleTimecodeToUs(timecode); - // Last byte of the three has some flags and the lacing value. - boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80; - boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08; - int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1; + // Last byte of the three has some flags and the lacing value. + boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80; + boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08; + int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1; - // Validate lacing and set info into sample holder. - switch (lacing) { - case LACING_NONE: - long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; - simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; - 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: - case LACING_XIPH: - default: - throw new IllegalStateException("Lacing mode " + lacing + " not supported"); - } + // Validate lacing and set info into sample holder. + switch (lacing) { + case LACING_NONE: + long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; + simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; + 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: + case LACING_XIPH: + default: + throw new ParserException("Lacing mode " + lacing + " not supported"); + } - if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) { - sampleHolder.replaceBuffer(sampleHolder.size); - } + if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) { + sampleHolder.replaceBuffer(sampleHolder.size); + } - ByteBuffer outputData = sampleHolder.data; - if (outputData == null) { - reader.skipBytes(inputStream, sampleHolder.size); - sampleHolder.size = 0; - } else { - reader.readBytes(inputStream, outputData, sampleHolder.size); - } - readResults |= RESULT_READ_SAMPLE; + ByteBuffer outputData = sampleHolder.data; + if (outputData == null) { + reader.skipBytes(inputStream, sampleHolder.size); + sampleHolder.size = 0; + } else { + reader.readBytes(inputStream, outputData, sampleHolder.size); + } + readResults |= RESULT_READ_SAMPLE; + break; + case ID_CODEC_PRIVATE: + codecPrivate = new byte[contentsSizeBytes]; + reader.readBytes(inputStream, codecPrivate, contentsSizeBytes); + break; + default: + // pass } return true; } @@ -372,16 +418,38 @@ public final class WebmExtractor implements Extractor { * *

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. + * the event that it can't be built, an {@link ParserException} will be thrown. */ - private void buildFormat() { + private void buildVideoFormat() throws ParserException { if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN && (format == null || format.width != pixelWidth || format.height != pixelHeight)) { format = MediaFormat.createVideoFormat( MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); readResults |= RESULT_READ_INIT; } else if (format == null) { - throw new IllegalStateException("Unable to build format"); + throw new ParserException("Unable to build format"); + } + } + + /** + * Build an audio {@link MediaFormat} containing recently gathered Audio information, if needed. + * + *

Replaces the previous {@link #format} only if audio channel count/sample rate have changed. + * {@link #format} is guaranteed to not be null after calling this method. + * + * @throws ParserException If an error occurs when parsing codec's private data or if the format + * can't be built. + */ + private void buildAudioFormat() throws ParserException { + if (channelCount != UNKNOWN && sampleRate != UNKNOWN + && (format == null || format.channelCount != channelCount + || format.sampleRate != sampleRate)) { + format = MediaFormat.createAudioFormat( + MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE, + sampleRate, channelCount, parseVorbisCodecPrivate()); + readResults |= RESULT_READ_INIT; + } else if (format == null) { + throw new ParserException("Unable to build format"); } } @@ -389,18 +457,18 @@ public final class WebmExtractor implements Extractor { * 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. + * the event that it can't be built, an {@link ParserException} will be thrown. */ - private void buildCues() { + private void buildCues() throws ParserException { if (segmentStartOffsetBytes == UNKNOWN) { - throw new IllegalStateException("Segment start/end offsets unknown"); + throw new ParserException("Segment start/end offsets unknown"); } else if (durationUs == UNKNOWN) { - throw new IllegalStateException("Duration unknown"); + throw new ParserException("Duration unknown"); } else if (cuesSizeBytes == UNKNOWN) { - throw new IllegalStateException("Cues size unknown"); + throw new ParserException("Cues size unknown"); } else if (cueTimesUs == null || cueClusterPositions == null || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { - throw new IllegalStateException("Invalid/missing cue points"); + throw new ParserException("Invalid/missing cue points"); } int cuePointsSize = cueTimesUs.size(); int[] sizes = new int[cuePointsSize]; @@ -423,6 +491,58 @@ public final class WebmExtractor implements Extractor { readResults |= RESULT_READ_INDEX; } + /** + * Parses Vorbis Codec Private data and adds it as initialization data to the {@link #format}. + * WebM Vorbis Codec Private data specification can be found + * here. + * + * @return ArrayList of byte arrays containing the initialization data on success. + * @throws ParserException If parsing codec private data fails. + */ + private ArrayList parseVorbisCodecPrivate() throws ParserException { + try { + if (codecPrivate[0] != 0x02) { + throw new ParserException("Error parsing vorbis codec private"); + } + int offset = 1; + int vorbisInfoLength = 0; + while (codecPrivate[offset] == (byte) 0xFF) { + vorbisInfoLength += 0xFF; + offset++; + } + vorbisInfoLength += codecPrivate[offset++]; + + int vorbisSkipLength = 0; + while (codecPrivate[offset] == (byte) 0xFF) { + vorbisSkipLength += 0xFF; + offset++; + } + vorbisSkipLength += codecPrivate[offset++]; + + if (codecPrivate[offset] != 0x01) { + throw new ParserException("Error parsing vorbis codec private"); + } + byte[] vorbisInfo = new byte[vorbisInfoLength]; + System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength); + offset += vorbisInfoLength; + if (codecPrivate[offset] != 0x03) { + throw new ParserException("Error parsing vorbis codec private"); + } + offset += vorbisSkipLength; + if (codecPrivate[offset] != 0x05) { + throw new ParserException("Error parsing vorbis codec private"); + } + byte[] vorbisBooks = new byte[codecPrivate.length - offset]; + System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset); + ArrayList initializationData = new ArrayList(2); + initializationData.add(vorbisInfo); + initializationData.add(vorbisBooks); + return initializationData; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing vorbis codec private"); + } + } + /** * Passes events through to {@link WebmExtractor} as * callbacks from {@link EbmlReader} are received. @@ -436,18 +556,19 @@ public final class WebmExtractor implements Extractor { @Override public void onMasterElementStart( - int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { + int id, long elementOffsetBytes, int headerSizeBytes, + long contentsSizeBytes) throws ParserException { WebmExtractor.this.onMasterElementStart( id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes); } @Override - public void onMasterElementEnd(int id) { + public void onMasterElementEnd(int id) throws ParserException { WebmExtractor.this.onMasterElementEnd(id); } @Override - public void onIntegerElement(int id, long value) { + public void onIntegerElement(int id, long value) throws ParserException { WebmExtractor.this.onIntegerElement(id, value); } @@ -457,14 +578,14 @@ public final class WebmExtractor implements Extractor { } @Override - public void onStringElement(int id, String value) { + public void onStringElement(int id, String value) throws ParserException { WebmExtractor.this.onStringElement(id, value); } @Override public boolean onBinaryElement( int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, - NonBlockingInputStream inputStream) { + NonBlockingInputStream inputStream) throws ParserException { return WebmExtractor.this.onBinaryElement( id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index e3443467f3..d10b04ed27 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -34,6 +34,8 @@ public class MimeTypes { public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_EC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm"; + public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From 7cd201c28b9e26a4bd9b6049af3a89add26f316f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Nov 2014 19:02:20 +0000 Subject: [PATCH 077/110] Add missing class. --- .../exoplayer/util/PriorityHandlerThread.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/util/PriorityHandlerThread.java diff --git a/library/src/main/java/com/google/android/exoplayer/util/PriorityHandlerThread.java b/library/src/main/java/com/google/android/exoplayer/util/PriorityHandlerThread.java new file mode 100644 index 0000000000..86f77ffa3b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/PriorityHandlerThread.java @@ -0,0 +1,43 @@ +/* + * 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.util; + +import android.os.HandlerThread; +import android.os.Process; + +/** + * A {@link HandlerThread} with a specified process priority. + */ +public class PriorityHandlerThread extends HandlerThread { + + private final int priority; + + /** + * @param name The name of the thread. + * @param priority The priority level. See {@link Process#setThreadPriority(int)} for details. + */ + public PriorityHandlerThread(String name, int priority) { + super(name); + this.priority = priority; + } + + @Override + public void run() { + Process.setThreadPriority(priority); + super.run(); + } + +} From 44bc01b28d3da0cf17368575b558bb66f38d289e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Nov 2014 19:02:32 +0000 Subject: [PATCH 078/110] Add receiver for HDMI plugged-in configuration changes. --- .../exoplayer/audio/AudioCapabilities.java | 97 +++++++++++++++++ .../audio/AudioCapabilitiesReceiver.java | 101 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilities.java create mode 100644 library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilities.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilities.java new file mode 100644 index 0000000000..24bcccaf03 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilities.java @@ -0,0 +1,97 @@ +/* + * 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.audio; + +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.AudioFormat; + +import java.util.HashSet; +import java.util.Set; + +/** + * Represents the set of audio formats a device is capable of playing back. + */ +@TargetApi(21) +public final class AudioCapabilities { + + private final Set supportedEncodings; + private final int maxChannelCount; + + /** + * Constructs new audio capabilities based on a set of supported encodings and a maximum channel + * count. + * + * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s + * {@code ENCODING_*} constants. + * @param maxChannelCount The maximum number of audio channels that can be played simultaneously. + */ + public AudioCapabilities(int[] supportedEncodings, int maxChannelCount) { + this.supportedEncodings = new HashSet(); + if (supportedEncodings != null) { + for (int i : supportedEncodings) { + this.supportedEncodings.add(i); + } + } + this.maxChannelCount = maxChannelCount; + } + + /** Returns whether the device supports playback of AC-3. */ + public boolean supportsAc3() { + return Util.SDK_INT >= 21 && supportedEncodings.contains(AudioFormat.ENCODING_AC3); + } + + /** Returns whether the device supports playback of enhanced AC-3. */ + public boolean supportsEAc3() { + return Util.SDK_INT >= 21 && supportedEncodings.contains(AudioFormat.ENCODING_E_AC3); + } + + /** Returns whether the device supports playback of 16-bit PCM. */ + public boolean supportsPcm() { + return supportedEncodings.contains(AudioFormat.ENCODING_PCM_16BIT); + } + + /** Returns the maximum number of channels the device can play at the same time. */ + public int getMaxChannelCount() { + return maxChannelCount; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AudioCapabilities)) { + return false; + } + AudioCapabilities audioCapabilities = (AudioCapabilities) other; + return supportedEncodings.equals(audioCapabilities.supportedEncodings) + && maxChannelCount == audioCapabilities.maxChannelCount; + } + + @Override + public int hashCode() { + return maxChannelCount + 31 * supportedEncodings.hashCode(); + } + + @Override + public String toString() { + return "AudioCapabilities[maxChannelCount=" + maxChannelCount + + ", supportedEncodings=" + supportedEncodings + "]"; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java new file mode 100644 index 0000000000..963bb344a8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java @@ -0,0 +1,101 @@ +/* + * 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.audio; + +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioFormat; +import android.media.AudioManager; + +/** + * Notifies a listener when the audio playback capabilities change. Call {@link #register} to start + * receiving notifications, and {@link #unregister} to stop. + */ +public final class AudioCapabilitiesReceiver { + + /** Listener notified when audio capabilities change. */ + public interface Listener { + + /** Called when the audio capabilities change. */ + void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities); + + } + + /** Default to stereo PCM on SDK <= 21 and when HDMI is unplugged. */ + private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = + new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2); + + private final Context context; + private final Listener listener; + private final BroadcastReceiver receiver; + + /** + * Constructs a new audio capabilities receiver. + * + * @param context Application context for registering to receive broadcasts. + * @param listener Listener to notify when audio capabilities change. + */ + public AudioCapabilitiesReceiver(Context context, Listener listener) { + this.context = Assertions.checkNotNull(context); + this.listener = Assertions.checkNotNull(listener); + this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null; + } + + /** + * Registers to notify the listener when audio capabilities change. The listener will immediately + * receive the current audio capabilities. It is important to call {@link #unregister} so that + * the listener can be garbage collected. + */ + @TargetApi(21) + public void register() { + if (receiver != null) { + context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); + } + + listener.onAudioCapabilitiesChanged(DEFAULT_AUDIO_CAPABILITIES); + } + + /** Unregisters to stop notifying the listener when audio capabilities change. */ + public void unregister() { + if (receiver != null) { + context.unregisterReceiver(receiver); + } + } + + @TargetApi(21) + private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (!action.equals(AudioManager.ACTION_HDMI_AUDIO_PLUG)) { + return; + } + + listener.onAudioCapabilitiesChanged( + new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS), + intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0))); + } + + } + +} From bc303b730ad34d421a14598f05cba26ed34eacb1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Nov 2014 19:03:04 +0000 Subject: [PATCH 079/110] Factor out AudioTrack from MediaCodecAudioTrackRenderer. AudioTrack contains the portions of MediaCodecAudioTrackRenderer that handle the platform AudioTrack instance, including synchronization (playback position smoothing), non-blocking writes and releasing. This refactoring should not affect the behavior of audio playback, and is in preparation for adding an Ac3PassthroughAudioTrackRenderer that will use the AudioTrack. --- .../exoplayer/demo/full/EventLogger.java | 4 +- .../demo/full/player/DemoPlayer.java | 6 +- .../MediaCodecAudioTrackRenderer.java | 635 ++-------------- .../android/exoplayer/audio/AudioTrack.java | 716 ++++++++++++++++++ 4 files changed, 800 insertions(+), 561 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java 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 84ea1b0f3e..7ccecc1285 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer.demo.full; import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.util.VerboseLogUtil; @@ -149,7 +149,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } @Override - public void onAudioTrackInitializationError(AudioTrackInitializationException e) { + public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { printInternalError("audioTrackInitializationError", e); } 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 b88ce89157..dfa900d18d 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 @@ -19,10 +19,10 @@ import com.google.android.exoplayer.DummyTrackRenderer; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; @@ -106,7 +106,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi */ public interface InternalErrorListener { void onRendererInitializationError(Exception e); - void onAudioTrackInitializationError(AudioTrackInitializationException e); + void onAudioTrackInitializationError(AudioTrack.InitializationException e); void onDecoderInitializationError(DecoderInitializationException e); void onCryptoError(CryptoException e); void onUpstreamError(int sourceId, IOException e); @@ -424,7 +424,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onAudioTrackInitializationError(AudioTrackInitializationException e) { + public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { if (internalErrorListener != null) { internalErrorListener.onAudioTrackInitializationError(e); } 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 dccb962fcd..3aa729e053 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -15,28 +15,21 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; -import android.media.AudioFormat; -import android.media.AudioManager; -import android.media.AudioTimestamp; -import android.media.AudioTrack; import android.media.MediaCodec; import android.media.MediaFormat; import android.media.audiofx.Virtualizer; -import android.os.ConditionVariable; import android.os.Handler; -import android.util.Log; -import java.lang.reflect.Method; import java.nio.ByteBuffer; /** - * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. + * Decodes and renders audio using {@link MediaCodec} and {@link android.media.AudioTrack}. */ @TargetApi(16) public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @@ -52,26 +45,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { * * @param e The corresponding exception. */ - void onAudioTrackInitializationError(AudioTrackInitializationException e); - - } - - /** - * Thrown when a failure occurs instantiating an audio track. - */ - public static class AudioTrackInitializationException extends Exception { - - /** - * The state as reported by {@link AudioTrack#getState()} - */ - public final int audioTrackState; - - public AudioTrackInitializationException(int audioTrackState, int sampleRate, - int channelConfig, int bufferSize) { - super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " - + channelConfig + ", " + bufferSize + ")"); - this.audioTrackState = audioTrackState; - } + void onAudioTrackInitializationError(AudioTrack.InitializationException e); } @@ -82,73 +56,12 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { */ public static final int MSG_SET_VOLUME = 1; - /** - * The default multiplication factor used when determining the size of the underlying - * {@link AudioTrack}'s buffer. - */ - public static final float DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR = 4; - - private static final String TAG = "MediaCodecAudioTrackRenderer"; - - private static final long MICROS_PER_SECOND = 1000000L; - - /** - * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more - * than this amount. - *

- * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; - - /** - * AudioTrack latencies are deemed impossibly large if they are greater than this amount. - *

- * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_AUDIO_TRACK_LATENCY_US = 10 * MICROS_PER_SECOND; - - private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; - private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; - private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; - - private static final int START_NOT_SET = 0; - private static final int START_IN_SYNC = 1; - private static final int START_NEED_SYNC = 2; - private final EventListener eventListener; - private final ConditionVariable audioTrackReleasingConditionVariable; - private final AudioTimestampCompat audioTimestampCompat; - private final long[] playheadOffsets; - private final float minBufferMultiplicationFactor; - private int nextPlayheadOffsetIndex; - private int playheadOffsetCount; - private long smoothedPlayheadOffsetUs; - private long lastPlayheadSampleTimeUs; - private boolean audioTimestampSet; - private long lastTimestampSampleTimeUs; - private long lastRawPlaybackHeadPosition; - private long rawPlaybackHeadWrapCount; - private int sampleRate; - private int frameSize; - private int channelConfig; - private int minBufferSize; - private int bufferSize; - - private AudioTrack audioTrack; - private Method audioTrackGetLatencyMethod; + private final AudioTrack audioTrack; private int audioSessionId; - private long submittedBytes; - private int audioTrackStartMediaTimeState; - private long audioTrackStartMediaTimeUs; - private long audioTrackResumeSystemTimeUs; - private long lastReportedCurrentPositionUs; - private long audioTrackLatencyUs; - private float volume; - private byte[] temporaryBuffer; - private int temporaryBufferOffset; - private int temporaryBufferSize; + private long currentPositionUs; /** * @param source The upstream source from which the renderer obtains samples. @@ -198,15 +111,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { */ public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) { - this(source, drmSessionManager, playClearSamplesWithoutKeys, - DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR, eventHandler, eventListener); + this(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener, + new AudioTrack()); } /** * @param source The upstream source from which the renderer obtains samples. - * @param minBufferMultiplicationFactor When instantiating an underlying {@link AudioTrack}, - * the size of the track's is calculated as this value multiplied by the minimum buffer size - * obtained from {@link AudioTrack#getMinBufferSize(int, int, int)}. The multiplication + * @param minBufferMultiplicationFactor When instantiating an underlying + * {@link android.media.AudioTrack}, the size of the track is calculated as this value + * multiplied by the minimum buffer size obtained from + * {@link android.media.AudioTrack#getMinBufferSize(int, int, int)}. The multiplication * factor must be greater than or equal to 1. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. @@ -226,9 +140,10 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { * begin in parallel with key acquisision. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param minBufferMultiplicationFactor When instantiating an underlying {@link AudioTrack}, - * the size of the track's is calculated as this value multiplied by the minimum buffer size - * obtained from {@link AudioTrack#getMinBufferSize(int, int, int)}. The multiplication + * @param minBufferMultiplicationFactor When instantiating an underlying + * {@link android.media.AudioTrack}, the size of the track is calculated as this value + * multiplied by the minimum buffer size obtained from + * {@link android.media.AudioTrack#getMinBufferSize(int, int, int)}. The multiplication * factor must be greater than or equal to 1. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. @@ -237,25 +152,31 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, float minBufferMultiplicationFactor, Handler eventHandler, EventListener eventListener) { + this(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener, + new AudioTrack(minBufferMultiplicationFactor)); + } + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisision. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioTrack Used for playing back decoded audio samples. + */ + public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener, + AudioTrack audioTrack) { super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); - Assertions.checkState(minBufferMultiplicationFactor >= 1); - this.minBufferMultiplicationFactor = minBufferMultiplicationFactor; this.eventListener = eventListener; - audioTrackReleasingConditionVariable = new ConditionVariable(true); - if (Util.SDK_INT >= 19) { - audioTimestampCompat = new AudioTimestampCompatV19(); - } else { - audioTimestampCompat = new NoopAudioTimestampCompat(); - } - if (Util.SDK_INT >= 18) { - try { - audioTrackGetLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class[]) null); - } catch (NoSuchMethodException e) { - // There's no guarantee this method exists. Do nothing. - } - } - playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; - volume = 1.0f; + this.audioTrack = Assertions.checkNotNull(audioTrack); + this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; } @Override @@ -271,104 +192,12 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onEnabled(long positionUs, boolean joining) { super.onEnabled(positionUs, joining); - lastReportedCurrentPositionUs = Long.MIN_VALUE; - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - super.doSomeWork(positionUs, elapsedRealtimeUs); - maybeSampleSyncParams(); + currentPositionUs = Long.MIN_VALUE; } @Override protected void onOutputFormatChanged(MediaFormat format) { - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int channelConfig; - switch (channelCount) { - case 1: - channelConfig = AudioFormat.CHANNEL_OUT_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - case 6: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - case 8: - channelConfig = AudioFormat.CHANNEL_OUT_7POINT1; - break; - 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); - this.bufferSize = (int) (minBufferMultiplicationFactor * minBufferSize); - this.frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels. - } - - private void initAudioTrack() throws ExoPlaybackException { - // If we're asynchronously releasing a previous audio track then we block until it has been - // released. This guarantees that we cannot end up in a state where we have multiple audio - // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust - // the shared memory that's available for audio track buffers. This would in turn cause the - // initialization of the audio track to fail. - audioTrackReleasingConditionVariable.block(); - if (audioSessionId == 0) { - audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, - AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); - checkAudioTrackInitialized(); - audioSessionId = audioTrack.getAudioSessionId(); - onAudioSessionId(audioSessionId); - } else { - // Re-attach to the same audio session. - audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, - AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM, audioSessionId); - checkAudioTrackInitialized(); - } - setVolume(volume); - if (getState() == TrackRenderer.STATE_STARTED) { - audioTrackResumeSystemTimeUs = System.nanoTime() / 1000; - audioTrack.play(); - } - } - - /** - * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this - * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an - * exception is thrown. - * - * @throws ExoPlaybackException If {@link #audioTrack} has not been successfully initialized. - */ - private void checkAudioTrackInitialized() throws ExoPlaybackException { - int audioTrackState = audioTrack.getState(); - if (audioTrackState == AudioTrack.STATE_INITIALIZED) { - return; - } - // The track is not successfully initialized. Release and null the track. - try { - audioTrack.release(); - } catch (Exception e) { - // The track has already failed to initialize, so it wouldn't be that surprising if release - // were to fail too. Swallow the exception. - } finally { - audioTrack = null; - } - // Propagate the relevant exceptions. - AudioTrackInitializationException exception = new AudioTrackInitializationException( - audioTrackState, sampleRate, channelConfig, bufferSize); - notifyAudioTrackInitializationError(exception); - throw new ExoPlaybackException(exception); + audioTrack.reconfigure(format); } /** @@ -387,51 +216,15 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { // Do nothing. } - private void releaseAudioTrack() { - if (audioTrack != null) { - submittedBytes = 0; - temporaryBufferSize = 0; - lastRawPlaybackHeadPosition = 0; - rawPlaybackHeadWrapCount = 0; - audioTrackStartMediaTimeUs = 0; - audioTrackStartMediaTimeState = START_NOT_SET; - resetSyncParams(); - int playState = audioTrack.getPlayState(); - if (playState == AudioTrack.PLAYSTATE_PLAYING) { - audioTrack.pause(); - } - // AudioTrack.release can take some time, so we call it on a background thread. - final AudioTrack toRelease = audioTrack; - audioTrack = null; - audioTrackReleasingConditionVariable.close(); - new Thread() { - @Override - public void run() { - try { - toRelease.release(); - } finally { - audioTrackReleasingConditionVariable.open(); - } - } - }.start(); - } - } - @Override protected void onStarted() { super.onStarted(); - if (audioTrack != null) { - audioTrackResumeSystemTimeUs = System.nanoTime() / 1000; - audioTrack.play(); - } + audioTrack.play(); } @Override protected void onStopped() { - if (audioTrack != null) { - resetSyncParams(); - audioTrack.pause(); - } + audioTrack.pause(); super.onStopped(); } @@ -439,149 +232,34 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { protected boolean isEnded() { // We've exhausted the output stream, and the AudioTrack has either played all of the data // submitted, or has been fed insufficient data to begin playback. - return super.isEnded() && (getPendingFrameCount() == 0 || submittedBytes < minBufferSize); + return super.isEnded() && (!audioTrack.hasPendingData() + || !audioTrack.hasEnoughDataToBeginPlayback()); } @Override protected boolean isReady() { - return getPendingFrameCount() > 0 + return audioTrack.hasPendingData() || (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL); } - /** - * This method uses a variety of techniques to compute the current position: - * - * 1. Prior to playback having started, calls up to the super class to obtain the pending seek - * position. - * 2. During playback, uses AudioTimestamps obtained from AudioTrack.getTimestamp on supported - * devices. - * 3. Else, derives a smoothed position by sampling the AudioTrack's frame position. - */ @Override protected long getCurrentPositionUs() { - long systemClockUs = System.nanoTime() / 1000; - long currentPositionUs; - if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET) { - // The AudioTrack hasn't started. - currentPositionUs = super.getCurrentPositionUs(); - } else if (audioTimestampSet) { - // How long ago in the past the audio timestamp is (negative if it's in the future) - long presentationDiff = systemClockUs - (audioTimestampCompat.getNanoTime() / 1000); - long framesDiff = durationUsToFrames(presentationDiff); - // The position of the frame that's currently being presented. - long currentFramePosition = audioTimestampCompat.getFramePosition() + framesDiff; - currentPositionUs = framesToDurationUs(currentFramePosition) + audioTrackStartMediaTimeUs; + long audioTrackCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); + if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) { + // Use the super class position before audio playback starts. + currentPositionUs = Math.max(currentPositionUs, super.getCurrentPositionUs()); } else { - if (playheadOffsetCount == 0) { - // The AudioTrack has started, but we don't have any samples to compute a smoothed position. - currentPositionUs = getPlayheadPositionUs() + audioTrackStartMediaTimeUs; - } else { - // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the - // system clock (and a smoothed offset between it and the playhead position) so as to - // prevent jitter in the reported positions. - currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + audioTrackStartMediaTimeUs; - } - if (!isEnded()) { - currentPositionUs -= audioTrackLatencyUs; - } + // Make sure we don't ever report time moving backwards. + currentPositionUs = Math.max(currentPositionUs, audioTrackCurrentPositionUs); } - // Make sure we don't ever report time moving backwards as a result of smoothing or switching - // between the various code paths above. - currentPositionUs = Math.max(lastReportedCurrentPositionUs, currentPositionUs); - lastReportedCurrentPositionUs = currentPositionUs; return currentPositionUs; } - private void maybeSampleSyncParams() { - if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET - || getState() != STATE_STARTED) { - // The AudioTrack isn't playing. - return; - } - long playheadPositionUs = getPlayheadPositionUs(); - if (playheadPositionUs == 0) { - // The AudioTrack hasn't output anything yet. - return; - } - long systemClockUs = System.nanoTime() / 1000; - if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { - // Take a new sample and update the smoothed offset between the system clock and the playhead. - playheadOffsets[nextPlayheadOffsetIndex] = playheadPositionUs - systemClockUs; - nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; - if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { - playheadOffsetCount++; - } - lastPlayheadSampleTimeUs = systemClockUs; - smoothedPlayheadOffsetUs = 0; - for (int i = 0; i < playheadOffsetCount; i++) { - smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; - } - } - - if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { - audioTimestampSet = audioTimestampCompat.update(audioTrack); - if (audioTimestampSet) { - // Perform sanity checks on the timestamp. - long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000; - if (audioTimestampUs < audioTrackResumeSystemTimeUs) { - // The timestamp corresponds to a time before the track was most recently resumed. - audioTimestampSet = false; - } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - // The timestamp time base is probably wrong. - audioTimestampSet = false; - Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", " - + audioTimestampUs + ", " + systemClockUs); - } - } - if (audioTrackGetLatencyMethod != null) { - try { - // Compute the audio track latency, excluding the latency due to the buffer (leaving - // latency due to the mixer and audio hardware driver). - audioTrackLatencyUs = - (Integer) audioTrackGetLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - - framesToDurationUs(bufferSize / frameSize); - // Sanity check that the latency is non-negative. - audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0); - // Sanity check that the latency isn't too large. - if (audioTrackLatencyUs > MAX_AUDIO_TRACK_LATENCY_US) { - Log.w(TAG, "Ignoring impossibly large audio latency: " + audioTrackLatencyUs); - audioTrackLatencyUs = 0; - } - } catch (Exception e) { - // The method existed, but doesn't work. Don't try again. - audioTrackGetLatencyMethod = null; - } - } - lastTimestampSampleTimeUs = systemClockUs; - } - } - - private void resetSyncParams() { - smoothedPlayheadOffsetUs = 0; - playheadOffsetCount = 0; - nextPlayheadOffsetIndex = 0; - lastPlayheadSampleTimeUs = 0; - audioTimestampSet = false; - lastTimestampSampleTimeUs = 0; - } - - private long getPlayheadPositionUs() { - return framesToDurationUs(getPlaybackHeadPosition()); - } - - private long framesToDurationUs(long frameCount) { - return (frameCount * MICROS_PER_SECOND) / sampleRate; - } - - private long durationUsToFrames(long durationUs) { - return (durationUs * sampleRate) / MICROS_PER_SECOND; - } - @Override protected void onDisabled() { - audioSessionId = 0; + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; try { - releaseAudioTrack(); + audioTrack.reset(); } finally { super.onDisabled(); } @@ -591,8 +269,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { protected void seekTo(long positionUs) throws ExoPlaybackException { super.seekTo(positionUs); // TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed. - releaseAudioTrack(); - lastReportedCurrentPositionUs = Long.MIN_VALUE; + audioTrack.reset(); + currentPositionUs = Long.MIN_VALUE; } @Override @@ -602,74 +280,39 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); codecCounters.skippedOutputBufferCount++; - if (audioTrackStartMediaTimeState == START_IN_SYNC) { - // Skipping the sample will push track time out of sync. We'll need to sync again. - audioTrackStartMediaTimeState = START_NEED_SYNC; - } + audioTrack.handleDiscontinuity(); return true; } - if (temporaryBufferSize == 0) { - // This is the first time we've seen this {@code buffer}. - // Note: presentationTimeUs corresponds to the end of the sample, not the start. - long bufferStartTime = bufferInfo.presentationTimeUs - - framesToDurationUs(bufferInfo.size / frameSize); - if (audioTrackStartMediaTimeState == START_NOT_SET) { - audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime); - audioTrackStartMediaTimeState = START_IN_SYNC; - } else { - // Sanity check that bufferStartTime is consistent with the expected value. - long expectedBufferStartTime = audioTrackStartMediaTimeUs - + framesToDurationUs(submittedBytes / frameSize); - if (audioTrackStartMediaTimeState == START_IN_SYNC - && Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { - Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " - + bufferStartTime + "]"); - audioTrackStartMediaTimeState = START_NEED_SYNC; - } - if (audioTrackStartMediaTimeState == START_NEED_SYNC) { - // Adjust audioTrackStartMediaTimeUs to be consistent with the current buffer's start - // time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to - // allow time to jump backwards if it really wants to. - audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); - audioTrackStartMediaTimeState = START_IN_SYNC; - lastReportedCurrentPositionUs = Long.MIN_VALUE; + // Initialize and start the audio track now. + if (!audioTrack.isInitialized()) { + try { + if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { + audioTrack.initialize(audioSessionId); + } else { + audioSessionId = audioTrack.initialize(); + onAudioSessionId(audioSessionId); } + } catch (AudioTrack.InitializationException e) { + notifyAudioTrackInitializationError(e); + throw new ExoPlaybackException(e); } - temporaryBufferSize = bufferInfo.size; - buffer.position(bufferInfo.offset); - if (Util.SDK_INT < 21) { - // Copy {@code buffer} into {@code temporaryBuffer}. - if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) { - temporaryBuffer = new byte[bufferInfo.size]; - } - buffer.get(temporaryBuffer, 0, bufferInfo.size); - temporaryBufferOffset = 0; + if (getState() == TrackRenderer.STATE_STARTED) { + audioTrack.play(); } } - if (audioTrack == null) { - initAudioTrack(); + int handleBufferResult = audioTrack.handleBuffer( + buffer, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs); + + // If we are out of sync, allow currentPositionUs to jump backwards. + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + currentPositionUs = Long.MIN_VALUE; } - int bytesWritten = 0; - if (Util.SDK_INT < 21) { - // Work out how many bytes we can write without the risk of blocking. - int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize); - int bytesToWrite = bufferSize - bytesPending; - if (bytesToWrite > 0) { - bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite); - bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - temporaryBufferOffset += bytesWritten; - } - } else { - bytesWritten = writeNonBlockingV21(audioTrack, buffer, temporaryBufferSize); - } - - temporaryBufferSize -= bytesWritten; - submittedBytes += bytesWritten; - if (temporaryBufferSize == 0) { + // Release the buffer if it was consumed. + if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { codec.releaseOutputBuffer(bufferIndex, false); codecCounters.renderedOutputBufferCount++; return true; @@ -678,66 +321,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { return false; } - @TargetApi(21) - private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { - return audioTrack.write(buffer, size, AudioTrack.WRITE_NON_BLOCKING); - } - - /** - * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as - * an unsigned 32 bit integer, which also wraps around periodically. This method returns the - * playback head position as a long that will only wrap around if the value exceeds - * {@link Long#MAX_VALUE} (which in practice will never happen). - * - * @return {@link AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} expressed as a - * long. - */ - private long getPlaybackHeadPosition() { - long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { - // The value must have wrapped around. - rawPlaybackHeadWrapCount++; - } - lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; - return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); - } - - private int getPendingFrameCount() { - return audioTrack == null - ? 0 : (int) (submittedBytes / frameSize - getPlaybackHeadPosition()); - } - @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == MSG_SET_VOLUME) { - setVolume((Float) message); + audioTrack.setVolume((Float) message); } else { super.handleMessage(messageType, message); } } - private void setVolume(float volume) { - this.volume = volume; - if (audioTrack != null) { - if (Util.SDK_INT >= 21) { - setVolumeV21(audioTrack, volume); - } else { - setVolumeV3(audioTrack, volume); - } - } - } - - @TargetApi(21) - private static void setVolumeV21(AudioTrack audioTrack, float volume) { - audioTrack.setVolume(volume); - } - - @SuppressWarnings("deprecation") - private static void setVolumeV3(AudioTrack audioTrack, float volume) { - audioTrack.setStereoVolume(volume, volume); - } - - private void notifyAudioTrackInitializationError(final AudioTrackInitializationException e) { + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override @@ -748,74 +341,4 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } } - /** - * Interface exposing the {@link AudioTimestamp} methods we need that were added in SDK 19. - */ - private interface AudioTimestampCompat { - - /** - * Returns true if the audioTimestamp was retrieved from the audioTrack. - */ - boolean update(AudioTrack audioTrack); - - long getNanoTime(); - - long getFramePosition(); - - } - - /** - * The AudioTimestampCompat implementation for SDK < 19 that does nothing or throws an exception. - */ - private static final class NoopAudioTimestampCompat implements AudioTimestampCompat { - - @Override - public boolean update(AudioTrack audioTrack) { - return false; - } - - @Override - public long getNanoTime() { - // Should never be called if initTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - @Override - public long getFramePosition() { - // Should never be called if initTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - } - - /** - * The AudioTimestampCompat implementation for SDK >= 19 that simply calls through to the actual - * implementations added in SDK 19. - */ - @TargetApi(19) - private static final class AudioTimestampCompatV19 implements AudioTimestampCompat { - - private final AudioTimestamp audioTimestamp; - - public AudioTimestampCompatV19() { - audioTimestamp = new AudioTimestamp(); - } - - @Override - public boolean update(AudioTrack audioTrack) { - return audioTrack.getTimestamp(audioTimestamp); - } - - @Override - public long getNanoTime() { - return audioTimestamp.nanoTime; - } - - @Override - public long getFramePosition() { - return audioTimestamp.framePosition; - } - - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java new file mode 100644 index 0000000000..d0a3ea4577 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -0,0 +1,716 @@ +/* + * 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.audio; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTimestamp; +import android.media.MediaFormat; +import android.os.ConditionVariable; +import android.util.Log; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +/** + * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles + * playback position smoothing, non-blocking writes and reconfiguration. + * + *

If {@link #isInitialized} returns {@code false}, the instance can be {@link #initialize}d. + * After initialization, start playback by calling {@link #play}. + * + *

Call {@link #handleBuffer} to write data for playback. + * + *

Call {@link #handleDiscontinuity} when a buffer is skipped. + * + *

Call {@link #reconfigure} when the output format changes. + * + *

Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance. + */ +@TargetApi(16) +public final class AudioTrack { + + /** + * Thrown when a failure occurs instantiating an {@link android.media.AudioTrack}. + */ + public static class InitializationException extends Exception { + + /** The state as reported by {@link android.media.AudioTrack#getState()}. */ + public final int audioTrackState; + + public InitializationException( + int audioTrackState, int sampleRate, int channelConfig, int bufferSize) { + super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " + + channelConfig + ", " + bufferSize + ")"); + this.audioTrackState = audioTrackState; + } + + } + + /** Returned in the result of {@link #handleBuffer} if the buffer was discontinuous. */ + public static final int RESULT_POSITION_DISCONTINUITY = 1; + /** Returned in the result of {@link #handleBuffer} if the buffer can be released. */ + public static final int RESULT_BUFFER_CONSUMED = 2; + + /** Represents an unset {@link android.media.AudioTrack} session identifier. */ + public static final int SESSION_ID_NOT_SET = 0; + + /** The default multiplication factor used when determining the size of the track's buffer. */ + public static final float DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR = 4; + + /** Returned by {@link #getCurrentPositionUs} when the position is not set. */ + public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; + + private static final String TAG = "AudioTrack"; + + private static final long MICROS_PER_SECOND = 1000000L; + + /** + * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more + * than this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_LATENCY_US = 10 * MICROS_PER_SECOND; + + private static final int START_NOT_SET = 0; + private static final int START_IN_SYNC = 1; + private static final int START_NEED_SYNC = 2; + + private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; + private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; + private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; + + private final ConditionVariable releasingConditionVariable; + private final AudioTimestampCompat audioTimestampCompat; + private final long[] playheadOffsets; + private final float minBufferMultiplicationFactor; + + private android.media.AudioTrack audioTrack; + private int sampleRate; + private int channelConfig; + private int encoding; + private int frameSize; + private int minBufferSize; + private int bufferSize; + + private int nextPlayheadOffsetIndex; + private int playheadOffsetCount; + private long smoothedPlayheadOffsetUs; + private long lastPlayheadSampleTimeUs; + private boolean audioTimestampSet; + private long lastTimestampSampleTimeUs; + private long lastRawPlaybackHeadPosition; + private long rawPlaybackHeadWrapCount; + + private Method getLatencyMethod; + private long submittedBytes; + private int startMediaTimeState; + private long startMediaTimeUs; + private long resumeSystemTimeUs; + private long latencyUs; + private float volume; + + private byte[] temporaryBuffer; + private int temporaryBufferOffset; + private int temporaryBufferSize; + + /** Constructs an audio track using the default minimum buffer size multiplier. */ + public AudioTrack() { + this(DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR); + } + + /** Constructs an audio track using the specified minimum buffer size multiplier. */ + public AudioTrack(float minBufferMultiplicationFactor) { + Assertions.checkArgument(minBufferMultiplicationFactor >= 1); + this.minBufferMultiplicationFactor = minBufferMultiplicationFactor; + releasingConditionVariable = new ConditionVariable(true); + if (Util.SDK_INT >= 19) { + audioTimestampCompat = new AudioTimestampCompatV19(); + } else { + audioTimestampCompat = new NoopAudioTimestampCompat(); + } + if (Util.SDK_INT >= 18) { + try { + getLatencyMethod = + android.media.AudioTrack.class.getMethod("getLatency", (Class[]) null); + } catch (NoSuchMethodException e) { + // There's no guarantee this method exists. Do nothing. + } + } + playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; + volume = 1.0f; + startMediaTimeState = START_NOT_SET; + } + + /** + * Returns whether the audio track has been successfully initialized via {@link #initialize} and + * not yet {@link #reset}. + */ + public boolean isInitialized() { + return audioTrack != null; + } + + /** + * Returns the playback position in the stream starting at zero, in microseconds, or + * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. + * + *

If the device supports it, the method uses the playback timestamp from + * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by + * sampling the {@link android.media.AudioTrack}'s frame position. + * + * @param sourceEnded Specify {@code true} if no more input buffers will be provided. + * @return The playback position relative to the start of playback, in microseconds. + */ + public long getCurrentPositionUs(boolean sourceEnded) { + if (!hasCurrentPositionUs()) { + return CURRENT_POSITION_NOT_SET; + } + + long systemClockUs = System.nanoTime() / 1000; + long currentPositionUs; + maybeSampleSyncParams(); + if (audioTimestampSet) { + // How long ago in the past the audio timestamp is (negative if it's in the future). + long presentationDiff = systemClockUs - (audioTimestampCompat.getNanoTime() / 1000); + long framesDiff = durationUsToFrames(presentationDiff); + // The position of the frame that's currently being presented. + long currentFramePosition = audioTimestampCompat.getFramePosition() + framesDiff; + currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs; + } else { + if (playheadOffsetCount == 0) { + // The AudioTrack has started, but we don't have any samples to compute a smoothed position. + currentPositionUs = getPlaybackPositionUs() + startMediaTimeUs; + } else { + // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the + // system clock (and a smoothed offset between it and the playhead position) so as to + // prevent jitter in the reported positions. + currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs; + } + if (!sourceEnded) { + currentPositionUs -= latencyUs; + } + } + + return currentPositionUs; + } + + /** + * Initializes the audio track for writing new buffers using {@link #handleBuffer}. + * + * @return The audio track session identifier. + */ + public int initialize() throws InitializationException { + return initialize(SESSION_ID_NOT_SET); + } + + /** + * Initializes the audio track for writing new buffers using {@link #handleBuffer}. + * + * @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to + * create a new one. + * @return The new (or re-used) session identifier. + */ + public int initialize(int sessionId) throws InitializationException { + // If we're asynchronously releasing a previous audio track then we block until it has been + // released. This guarantees that we cannot end up in a state where we have multiple audio + // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust + // the shared memory that's available for audio track buffers. This would in turn cause the + // initialization of the audio track to fail. + releasingConditionVariable.block(); + + if (sessionId == SESSION_ID_NOT_SET) { + audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, + channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM); + } else { + // Re-attach to the same audio session. + audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, + channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); + } + checkAudioTrackInitialized(); + setVolume(volume); + return audioTrack.getAudioSessionId(); + } + + /** + * Reconfigures the audio track to play back media in {@code format}. The encoding is assumed to + * be {@link AudioFormat#ENCODING_PCM_16BIT}. + */ + public void reconfigure(MediaFormat format) { + reconfigure(format, AudioFormat.ENCODING_PCM_16BIT, 0); + } + + /** + * Reconfigures the audio track to play back media in {@code format}. Buffers passed to + * {@link #handleBuffer} must using the specified {@code encoding}, which should be a constant + * from {@link AudioFormat}. + * + * @param format Specifies the channel count and sample rate to play back. + * @param encoding The format in which audio is represented. + * @param bufferSize The total size of the playback buffer in bytes. Specify 0 to use a buffer + * size based on the minimum for format. + */ + public void reconfigure(MediaFormat format, int encoding, int bufferSize) { + int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + int channelConfig; + switch (channelCount) { + case 1: + channelConfig = AudioFormat.CHANNEL_OUT_MONO; + break; + case 2: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + break; + case 6: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + case 8: + channelConfig = AudioFormat.CHANNEL_OUT_7POINT1; + break; + default: + throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + } + + int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + + // TODO: Does channelConfig determine channelCount? + 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; + } + + reset(); + + minBufferSize = android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding); + + this.encoding = encoding; + this.bufferSize = + bufferSize == 0 ? (int) (minBufferMultiplicationFactor * minBufferSize) : bufferSize; + this.sampleRate = sampleRate; + this.channelConfig = channelConfig; + + frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels. + } + + /** Starts/resumes playing audio if the audio track has been initialized. */ + public void play() { + if (isInitialized()) { + resumeSystemTimeUs = System.nanoTime() / 1000; + audioTrack.play(); + } + } + + /** Signals to the audio track that the next buffer is discontinuous with the previous buffer. */ + public void handleDiscontinuity() { + // Force resynchronization after a skipped buffer. + if (startMediaTimeState == START_IN_SYNC) { + startMediaTimeState = START_NEED_SYNC; + } + } + + /** + * Attempts to write {@code size} bytes from {@code buffer} at {@code offset} to the audio track. + * Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released + * (due to having been written), and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was + * discontinuous with previously written data. + * + * @param buffer The buffer containing audio data to play back. + * @param offset The offset in the buffer from which to consume data. + * @param size The number of bytes to consume from {@code buffer}. + * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. + * @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and + * {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously + * written data. + */ + public int handleBuffer(ByteBuffer buffer, int offset, int size, long presentationTimeUs) { + int result = 0; + + if (temporaryBufferSize == 0 && size != 0) { + // This is the first time we've seen this {@code buffer}. + // Note: presentationTimeUs corresponds to the end of the sample, not the start. + long bufferStartTime = presentationTimeUs - framesToDurationUs(bytesToFrames(size)); + if (startMediaTimeUs == START_NOT_SET) { + startMediaTimeUs = Math.max(0, bufferStartTime); + startMediaTimeState = START_IN_SYNC; + } else { + // Sanity check that bufferStartTime is consistent with the expected value. + long expectedBufferStartTime = startMediaTimeUs + + framesToDurationUs(bytesToFrames(submittedBytes)); + if (startMediaTimeState == START_IN_SYNC + && Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { + Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + + bufferStartTime + "]"); + startMediaTimeState = START_NEED_SYNC; + } + if (startMediaTimeState == START_NEED_SYNC) { + // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the + // number of bytes submitted. + startMediaTimeUs += (bufferStartTime - expectedBufferStartTime); + startMediaTimeState = START_IN_SYNC; + result = RESULT_POSITION_DISCONTINUITY; + } + } + } + + if (size == 0) { + return result; + } + + if (temporaryBufferSize == 0) { + temporaryBufferSize = size; + buffer.position(offset); + if (Util.SDK_INT < 21) { + // Copy {@code buffer} into {@code temporaryBuffer}. + if (temporaryBuffer == null || temporaryBuffer.length < size) { + temporaryBuffer = new byte[size]; + } + buffer.get(temporaryBuffer, 0, size); + temporaryBufferOffset = 0; + } + } + + int bytesWritten = 0; + if (Util.SDK_INT < 21) { + // Work out how many bytes we can write without the risk of blocking. + int bytesPending = (int) (submittedBytes - framesToBytes(getPlaybackPositionFrames())); + int bytesToWrite = bufferSize - bytesPending; + if (bytesToWrite > 0) { + bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite); + bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); + if (bytesWritten < 0) { + Log.w(TAG, "AudioTrack.write returned error code: " + bytesWritten); + } else { + temporaryBufferOffset += bytesWritten; + } + } + } else { + bytesWritten = writeNonBlockingV21(audioTrack, buffer, temporaryBufferSize); + } + + temporaryBufferSize -= bytesWritten; + submittedBytes += bytesWritten; + if (temporaryBufferSize == 0) { + result |= RESULT_BUFFER_CONSUMED; + } + + return result; + } + + @TargetApi(21) + private static int writeNonBlockingV21( + android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING); + } + + /** Returns whether the audio track has more data pending that will be played back. */ + public boolean hasPendingData() { + return audioTrack != null && bytesToFrames(submittedBytes) > getPlaybackPositionFrames(); + } + + /** Returns whether enough data has been supplied via {@link #handleBuffer} to begin playback. */ + public boolean hasEnoughDataToBeginPlayback() { + return submittedBytes >= minBufferSize; + } + + /** Sets the playback volume. */ + public void setVolume(float volume) { + this.volume = volume; + if (audioTrack != null) { + if (Util.SDK_INT >= 21) { + setVolumeV21(audioTrack, volume); + } else { + setVolumeV3(audioTrack, volume); + } + } + } + + @TargetApi(21) + private static void setVolumeV21(android.media.AudioTrack audioTrack, float volume) { + audioTrack.setVolume(volume); + } + + @SuppressWarnings("deprecation") + private static void setVolumeV3(android.media.AudioTrack audioTrack, float volume) { + audioTrack.setStereoVolume(volume, volume); + } + + /** Pauses playback. */ + public void pause() { + if (audioTrack != null) { + resetSyncParams(); + audioTrack.pause(); + } + } + + /** + * Releases resources associated with this instance asynchronously. Calling {@link #initialize} + * will block until the audio track has been released, so it is safe to initialize immediately + * after resetting. + */ + public void reset() { + if (audioTrack != null) { + submittedBytes = 0; + temporaryBufferSize = 0; + lastRawPlaybackHeadPosition = 0; + rawPlaybackHeadWrapCount = 0; + startMediaTimeUs = START_NOT_SET; + resetSyncParams(); + int playState = audioTrack.getPlayState(); + if (playState == android.media.AudioTrack.PLAYSTATE_PLAYING) { + audioTrack.pause(); + } + // AudioTrack.release can take some time, so we call it on a background thread. + final android.media.AudioTrack toRelease = audioTrack; + audioTrack = null; + releasingConditionVariable.close(); + new Thread() { + @Override + public void run() { + try { + toRelease.release(); + } finally { + releasingConditionVariable.open(); + } + } + }.start(); + } + } + + /** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */ + private boolean hasCurrentPositionUs() { + return isInitialized() && startMediaTimeUs != START_NOT_SET; + } + + /** Updates the audio track latency and playback position parameters. */ + private void maybeSampleSyncParams() { + if (!hasCurrentPositionUs()) { + return; + } + + long playbackPositionUs = getPlaybackPositionUs(); + if (playbackPositionUs == 0) { + // The AudioTrack hasn't output anything yet. + return; + } + long systemClockUs = System.nanoTime() / 1000; + if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { + // Take a new sample and update the smoothed offset between the system clock and the playhead. + playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs; + nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; + if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { + playheadOffsetCount++; + } + lastPlayheadSampleTimeUs = systemClockUs; + smoothedPlayheadOffsetUs = 0; + for (int i = 0; i < playheadOffsetCount; i++) { + smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; + } + } + + if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { + audioTimestampSet = audioTimestampCompat.update(audioTrack); + if (audioTimestampSet) { + // Perform sanity checks on the timestamp. + long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000; + if (audioTimestampUs < resumeSystemTimeUs) { + // The timestamp corresponds to a time before the track was most recently resumed. + audioTimestampSet = false; + } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + // The timestamp time base is probably wrong. + audioTimestampSet = false; + Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", " + + audioTimestampUs + ", " + systemClockUs); + } + } + if (getLatencyMethod != null) { + try { + // Compute the audio track latency, excluding the latency due to the buffer (leaving + // latency due to the mixer and audio hardware driver). + latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L + - framesToDurationUs(bytesToFrames(bufferSize)); + // Sanity check that the latency is non-negative. + latencyUs = Math.max(latencyUs, 0); + // Sanity check that the latency isn't too large. + if (latencyUs > MAX_LATENCY_US) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); + latencyUs = 0; + } + } catch (Exception e) { + // The method existed, but doesn't work. Don't try again. + getLatencyMethod = null; + } + } + lastTimestampSampleTimeUs = systemClockUs; + } + } + + /** + * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this + * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an + * exception is thrown. + * + * @throws ExoPlaybackException If {@link #audioTrack} has not been successfully initialized. + */ + private void checkAudioTrackInitialized() throws InitializationException { + int state = audioTrack.getState(); + if (state == android.media.AudioTrack.STATE_INITIALIZED) { + return; + } + // The track is not successfully initialized. Release and null the track. + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if release + // were to fail too. Swallow the exception. + } finally { + audioTrack = null; + } + + throw new InitializationException(state, sampleRate, channelConfig, bufferSize); + } + + /** + * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be + * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method + * returns the playback head position as a long that will only wrap around if the value exceeds + * {@link Long#MAX_VALUE} (which in practice will never happen). + * + * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} + * expressed as a long. + */ + private long getPlaybackPositionFrames() { + long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); + if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { + // The value must have wrapped around. + rawPlaybackHeadWrapCount++; + } + lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; + return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); + } + + private long getPlaybackPositionUs() { + return framesToDurationUs(getPlaybackPositionFrames()); + } + + private long framesToBytes(long frameCount) { + return frameCount * frameSize; + } + + private long bytesToFrames(long byteCount) { + return byteCount / frameSize; + } + + private long framesToDurationUs(long frameCount) { + return (frameCount * MICROS_PER_SECOND) / sampleRate; + } + + private long durationUsToFrames(long durationUs) { + return (durationUs * sampleRate) / MICROS_PER_SECOND; + } + + private void resetSyncParams() { + smoothedPlayheadOffsetUs = 0; + playheadOffsetCount = 0; + nextPlayheadOffsetIndex = 0; + lastPlayheadSampleTimeUs = 0; + audioTimestampSet = false; + lastTimestampSampleTimeUs = 0; + } + + /** + * Interface exposing the {@link android.media.AudioTimestamp} methods we need that were added in + * SDK 19. + */ + private interface AudioTimestampCompat { + + /** + * Returns true if the audioTimestamp was retrieved from the audioTrack. + */ + boolean update(android.media.AudioTrack audioTrack); + + long getNanoTime(); + + long getFramePosition(); + + } + + /** + * The AudioTimestampCompat implementation for SDK < 19 that does nothing or throws an exception. + */ + private static final class NoopAudioTimestampCompat implements AudioTimestampCompat { + + @Override + public boolean update(android.media.AudioTrack audioTrack) { + return false; + } + + @Override + public long getNanoTime() { + // Should never be called if initTimestamp() returned false. + throw new UnsupportedOperationException(); + } + + @Override + public long getFramePosition() { + // Should never be called if initTimestamp() returned false. + throw new UnsupportedOperationException(); + } + + } + + /** + * The AudioTimestampCompat implementation for SDK >= 19 that simply calls through to the actual + * implementations added in SDK 19. + */ + @TargetApi(19) + private static final class AudioTimestampCompatV19 implements AudioTimestampCompat { + + private final AudioTimestamp audioTimestamp; + + public AudioTimestampCompatV19() { + audioTimestamp = new AudioTimestamp(); + } + + @Override + public boolean update(android.media.AudioTrack audioTrack) { + return audioTrack.getTimestamp(audioTimestamp); + } + + @Override + public long getNanoTime() { + return audioTimestamp.nanoTime; + } + + @Override + public long getFramePosition() { + return audioTimestamp.framePosition; + } + + } + +} From 255c3b27f6ee08e09007addb6f177bebc62d8c0e Mon Sep 17 00:00:00 2001 From: Jonas Larsson Date: Fri, 14 Nov 2014 16:13:13 -0800 Subject: [PATCH 080/110] MediaCodecTrackRenderer: Avoid excessive garbage generation Looking up a long in a HashSet auto boxes the long and leaves it for the GC. As decodeOnly is relatively infrequent it's much better to do a simple linear search in a List. That way we can avoid boxing every incoming time stamp value. In the general case this will be linear searching in an empty list, a very fast operation. Signed-off-by: Jonas Larsson --- .../exoplayer/MediaCodecTrackRenderer.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 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 1fa103f395..d41211f3c2 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -30,7 +30,8 @@ import android.os.SystemClock; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.HashSet; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -141,7 +142,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { private final SampleSource source; private final SampleHolder sampleHolder; private final MediaFormatHolder formatHolder; - private final HashSet decodeOnlyPresentationTimestamps; + private final List decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; private final EventListener eventListener; protected final Handler eventHandler; @@ -191,7 +192,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { codecCounters = new CodecCounters(); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); formatHolder = new MediaFormatHolder(); - decodeOnlyPresentationTimestamps = new HashSet(); + decodeOnlyPresentationTimestamps = new ArrayList(); outputBufferInfo = new MediaCodec.BufferInfo(); } @@ -738,12 +739,11 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { return false; } - boolean decodeOnly = decodeOnlyPresentationTimestamps.contains( - outputBufferInfo.presentationTimeUs); + int decodeOnlyIdx = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs); if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputBufferInfo, outputIndex, decodeOnly)) { - if (decodeOnly) { - decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs); + outputBufferInfo, outputIndex, decodeOnlyIdx >= 0)) { + if (decodeOnlyIdx >= 0) { + decodeOnlyPresentationTimestamps.remove(decodeOnlyIdx); } else { currentPositionUs = outputBufferInfo.presentationTimeUs; } @@ -794,4 +794,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } } + private int getDecodeOnlyIndex(long presentationTimeUs) { + final int size = decodeOnlyPresentationTimestamps.size(); + for (int i = 0; i < size; i++) { + if (decodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) { + return i; + } + } + return -1; + } } From 127bcd18c386108aa6ee5a9370ea2ad6a9e3855d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Nov 2014 15:58:26 +0000 Subject: [PATCH 081/110] Bring internal/external branches closer. - Unredact internal error ids. - Move images. --- .../{doc_src => doc}/images/exoplayer_diagrams.svg | 0 .../images/exoplayer_playbackstate.png | Bin library/{doc_src => doc}/images/exoplayer_state.png | Bin .../images/exoplayer_threading_model.png | Bin .../{doc_src => doc}/images/trackrenderer_state.png | Bin .../exoplayer/MediaCodecAudioTrackRenderer.java | 2 +- .../android/exoplayer/MediaCodecTrackRenderer.java | 2 +- .../exoplayer/MediaCodecVideoTrackRenderer.java | 2 +- .../parser/mp4/FragmentedMp4Extractor.java | 2 +- .../android/exoplayer/text/TextTrackRenderer.java | 2 +- 10 files changed, 5 insertions(+), 5 deletions(-) rename library/{doc_src => doc}/images/exoplayer_diagrams.svg (100%) rename library/{doc_src => doc}/images/exoplayer_playbackstate.png (100%) rename library/{doc_src => doc}/images/exoplayer_state.png (100%) rename library/{doc_src => doc}/images/exoplayer_threading_model.png (100%) rename library/{doc_src => doc}/images/trackrenderer_state.png (100%) diff --git a/library/doc_src/images/exoplayer_diagrams.svg b/library/doc/images/exoplayer_diagrams.svg similarity index 100% rename from library/doc_src/images/exoplayer_diagrams.svg rename to library/doc/images/exoplayer_diagrams.svg diff --git a/library/doc_src/images/exoplayer_playbackstate.png b/library/doc/images/exoplayer_playbackstate.png similarity index 100% rename from library/doc_src/images/exoplayer_playbackstate.png rename to library/doc/images/exoplayer_playbackstate.png diff --git a/library/doc_src/images/exoplayer_state.png b/library/doc/images/exoplayer_state.png similarity index 100% rename from library/doc_src/images/exoplayer_state.png rename to library/doc/images/exoplayer_state.png diff --git a/library/doc_src/images/exoplayer_threading_model.png b/library/doc/images/exoplayer_threading_model.png similarity index 100% rename from library/doc_src/images/exoplayer_threading_model.png rename to library/doc/images/exoplayer_threading_model.png diff --git a/library/doc_src/images/trackrenderer_state.png b/library/doc/images/trackrenderer_state.png similarity index 100% rename from library/doc_src/images/trackrenderer_state.png rename to library/doc/images/trackrenderer_state.png 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 3aa729e053..5c8a0056d6 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -268,7 +268,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void seekTo(long positionUs) throws ExoPlaybackException { super.seekTo(positionUs); - // TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed. + // TODO: Try and re-use the same AudioTrack instance once [Internal: b/7941810] is fixed. audioTrack.reset(); currentPositionUs = Long.MIN_VALUE; } 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 fdd4020769..feb9cf2996 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -467,7 +467,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { waitingForFirstSyncFrame = true; decodeOnlyPresentationTimestamps.clear(); // Workaround for framework bugs. - // See [redacted], [redacted], [redacted]. + // See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366]. if (Util.SDK_INT >= 18) { codec.flush(); } else { 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 858597e3ac..397f83cb88 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -103,7 +103,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } - // TODO: Use MediaFormat constants if these get exposed through the API. See [redacted]. + // TODO: Use MediaFormat constants if these get exposed through the API. See [Internal: b/14127601]. private static final String KEY_CROP_LEFT = "crop-left"; private static final String KEY_CROP_RIGHT = "crop-right"; private static final String KEY_CROP_BOTTOM = "crop-bottom"; 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 1229e87d5b..ad5308d7c0 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 @@ -583,7 +583,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (atomType == Atom.TYPE_mp4a || atomType == Atom.TYPE_enca) { if (childAtomType == Atom.TYPE_esds) { initializationData = parseEsdsFromParent(parent, childStartPosition); - // TODO: Do we really need to do this? See [redacted] + // TODO: Do we really need to do this? See [Internal: b/10903778] // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. Pair audioSpecificConfig = CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); 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 26ed0145c4..4fd581bf56 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 @@ -247,7 +247,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { @Override protected boolean isReady() { // Don't block playback whilst subtitles are loading. - // Note: To change this behavior, it will be necessary to consider [redacted]. + // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941]. return true; } From 2a832fd3c495c224fea934335a7b7c62bd80cbe5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Nov 2014 11:03:47 +0000 Subject: [PATCH 082/110] Minor stylistic tweaks. --- .../android/exoplayer/MediaCodecTrackRenderer.java | 9 +++++---- 1 file changed, 5 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 224629b85e..271e8ff461 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -737,11 +737,11 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { return false; } - int decodeOnlyIdx = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs); + int decodeOnlyIndex = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs); if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputBufferInfo, outputIndex, decodeOnlyIdx >= 0)) { - if (decodeOnlyIdx >= 0) { - decodeOnlyPresentationTimestamps.remove(decodeOnlyIdx); + outputBufferInfo, outputIndex, decodeOnlyIndex != -1)) { + if (decodeOnlyIndex != -1) { + decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex); } else { currentPositionUs = outputBufferInfo.presentationTimeUs; } @@ -794,4 +794,5 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } return -1; } + } From 33c37ebc82a9ffbe59d040b3be09f34e3a465e85 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Nov 2014 11:04:38 +0000 Subject: [PATCH 083/110] Fix AudioTrack position reporting whilst paused. Issue: #158 --- .../google/android/exoplayer/audio/AudioTrack.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index d0a3ea4577..ac49449eac 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.audio; -import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; @@ -192,9 +191,12 @@ public final class AudioTrack { return CURRENT_POSITION_NOT_SET; } + if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PLAYING) { + maybeSampleSyncParams(); + } + long systemClockUs = System.nanoTime() / 1000; long currentPositionUs; - maybeSampleSyncParams(); if (audioTimestampSet) { // How long ago in the past the audio timestamp is (negative if it's in the future). long presentationDiff = systemClockUs - (audioTimestampCompat.getNanoTime() / 1000); @@ -508,10 +510,6 @@ public final class AudioTrack { /** Updates the audio track latency and playback position parameters. */ private void maybeSampleSyncParams() { - if (!hasCurrentPositionUs()) { - return; - } - long playbackPositionUs = getPlaybackPositionUs(); if (playbackPositionUs == 0) { // The AudioTrack hasn't output anything yet. @@ -574,7 +572,7 @@ public final class AudioTrack { * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an * exception is thrown. * - * @throws ExoPlaybackException If {@link #audioTrack} has not been successfully initialized. + * @throws InitializationException If {@link #audioTrack} has not been successfully initialized. */ private void checkAudioTrackInitialized() throws InitializationException { int state = audioTrack.getState(); From 2798b430cabcb547faeac41056ed39d914825592 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Nov 2014 12:23:29 +0000 Subject: [PATCH 084/110] Delete spurious file. --- library/.project~ | 53 ----------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 library/.project~ diff --git a/library/.project~ b/library/.project~ deleted file mode 100644 index 5d04c5fa5c..0000000000 --- a/library/.project~ +++ /dev/null @@ -1,53 +0,0 @@ - - - ExoPlayerLib - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - - - 1363908161147 - - 22 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-false-false-BUILD - - - - 1363908161148 - - 10 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-true-false-build - - - - From 0ce3e6e8a6adf5bcecf660abb23e91fcef928cb0 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Wed, 19 Nov 2014 15:30:54 -0800 Subject: [PATCH 085/110] fix compatibility with android gradle plugin 0.14 --- build.gradle | 2 +- library/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a444cfb512..68d066ea65 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.12.+' + classpath 'com.android.tools.build:gradle:0.14.+' } } diff --git a/library/build.gradle b/library/build.gradle index 709793acf5..5d4d8f737c 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -11,7 +11,7 @@ // 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. -apply plugin: 'android-library' +apply plugin: 'com.android.library' android { compileSdkVersion 19 @@ -24,7 +24,7 @@ android { buildTypes { release { - runProguard false + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } } From 5447081d17eb209f9fee8956ce0251214bdcdb7a Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Thu, 20 Nov 2014 12:18:13 -0800 Subject: [PATCH 086/110] gradle: 2.2.1-rc-1 wrapper jar and distribution url --- gradle/wrapper/gradle-wrapper.jar | Bin 50508 -> 51026 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d5c591c9c532da774b062aa6d7ab16405ff8700a..747bb13173370f361efaaf7935f6061d1dc2c61d 100644 GIT binary patch delta 27300 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 delta 26785 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 wY6DcDtQta4XaAoT`TavE4aP` Date: Thu, 20 Nov 2014 12:18:35 -0800 Subject: [PATCH 087/110] gradle: fix demo build.gradle --- demo/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/build.gradle b/demo/build.gradle index c8db67dba5..60dc9399d0 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -11,7 +11,7 @@ // 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. -apply plugin: 'android' +apply plugin: 'com.android.application' android { compileSdkVersion 19 @@ -23,7 +23,7 @@ android { } buildTypes { release { - runProguard false + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } } From 66c48a1151f5fa9e59d8ef3a91e5e789fe10b57c Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Thu, 20 Nov 2014 12:18:54 -0800 Subject: [PATCH 088/110] gradle: compileSdkVersion 21 fixes broken import statements --- library/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/build.gradle b/library/build.gradle index 5d4d8f737c..8ccf3cfaca 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -14,7 +14,7 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 19 + compileSdkVersion 21 buildToolsVersion "19.1" defaultConfig { From 9658534b937812f20b12016c34d39d0f3bb3b882 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Thu, 20 Nov 2014 15:54:48 -0800 Subject: [PATCH 089/110] demo: compileSdkVersion 21 (was 19) --- demo/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/build.gradle b/demo/build.gradle index 60dc9399d0..91a06b8b07 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -14,7 +14,7 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 19 + compileSdkVersion 21 buildToolsVersion "19.1" defaultConfig { From 69c7cb09c8389f3ba5f9be8165a2db754c244880 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 21 Nov 2014 17:54:55 +0000 Subject: [PATCH 090/110] Correctly handle redirection when requesting manifests. --- .../java/com/google/android/exoplayer/util/ManifestFetcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 1c2c4cdcbf..9cf8274207 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -293,7 +293,7 @@ public class ManifestFetcher implements Loader.Callback { inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); result = parser.parse(inputStream, inputEncoding, contentId, - Util.parseBaseUri(manifestUrl)); + Util.parseBaseUri(connection.getURL().toString())); } finally { if (inputStream != null) { inputStream.close(); From 7dfebc2e11f4d696e0e729adafe7c8a469bee669 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Nov 2014 11:22:54 +0000 Subject: [PATCH 091/110] Make default retry count public. --- .../google/android/exoplayer/chunk/ChunkSampleSource.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 263a47a5a5..f16fc4c4df 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 @@ -134,14 +134,17 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1; + private static final int STATE_UNPREPARED = 0; private static final int STATE_PREPARED = 1; private static final int STATE_ENABLED = 2; private static final int NO_RESET_PENDING = -1; - private static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1; - private final int eventSourceId; private final LoadControl loadControl; private final ChunkSource chunkSource; From ab00a4da03bfc5ddcc2ed29b0be31cc6308dabe7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Nov 2014 11:23:15 +0000 Subject: [PATCH 092/110] Allow non-strict webvtt parsing. --- .../exoplayer/text/webvtt/WebvttParser.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java index baace215f5..036c6116a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java @@ -63,6 +63,16 @@ public class WebvttParser implements SubtitleParser { private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+"); private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+"); + private final boolean strictParsing; + + public WebvttParser() { + this(true); + } + + public WebvttParser(boolean strictParsing) { + this.strictParsing = strictParsing; + } + @Override public WebvttSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs) throws IOException { @@ -108,7 +118,7 @@ public class WebvttParser implements SubtitleParser { Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line); if (!matcher.find()) { - throw new ParserException("Expected webvtt metadata header; got: " + line); + handleNoncompliantLine(line); } if (line.startsWith("X-TIMESTAMP-MAP")) { @@ -182,6 +192,12 @@ public class WebvttParser implements SubtitleParser { return startTimeUs; } + protected void handleNoncompliantLine(String line) throws ParserException { + if (strictParsing) { + throw new ParserException("Unexpected line: " + line); + } + } + private static long parseTimestampUs(String s) throws NumberFormatException { if (!s.matches(WEBVTT_TIMESTAMP_STRING)) { throw new NumberFormatException("has invalid format"); From 1a557a06c1a25989d9fc644c64762c4561a9a218 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:11:43 +0000 Subject: [PATCH 093/110] Support SmoothStreaming repeated chunk tags. --- .../SmoothStreamingManifestParser.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index 1114c1e4d0..20aea8ad32 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -450,6 +450,7 @@ public class SmoothStreamingManifestParser implements ManifestParser tracks; @@ -504,9 +505,18 @@ public class SmoothStreamingManifestParser implements ManifestParser 1 && lastChunkDuration == -1L) { + throw new ParserException("Repeated chunk with unspecified duration"); + } + for (int i = 1; i < repeatCount; i++) { + chunkIndex++; + startTimes.add(startTime + (lastChunkDuration * i)); + } } private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { From c5342630328fcda01eeec01784d63d43b0c0d352 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:12:46 +0000 Subject: [PATCH 094/110] Enhance parsing of xs:duration to support year/month/day. --- .../google/android/exoplayer/util/Util.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 78c2d267d6..4c08c5a528 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -55,7 +55,8 @@ public final class Util { + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = - Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$"); + Pattern.compile("^P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private Util() {} @@ -274,11 +275,19 @@ public final class Util { public static long parseXsDuration(String value) { Matcher matcher = XS_DURATION_PATTERN.matcher(value); if (matcher.matches()) { - String hours = matcher.group(2); - double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0; - String minutes = matcher.group(4); + // Durations containing years and months aren't completely defined. We assume there are + // 30.4368 days in a month, and 365.242 days in a year. + String years = matcher.group(2); + double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0; + String months = matcher.group(4); + durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0; + String days = matcher.group(6); + durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0; + String hours = matcher.group(9); + durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0; + String minutes = matcher.group(11); durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0; - String seconds = matcher.group(6); + String seconds = matcher.group(13); durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0; return (long) (durationSeconds * 1000); } else { From 2969bba60fd502bbac2d11cdab399fdd3b438ea3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:14:19 +0000 Subject: [PATCH 095/110] Fix timestamp rollover issue for DASH live. The timestamp scaling in SegmentBase.getSegmentTimeUs was overflowing for some streams. Apply a similar trick to that applied in the SmoothStreaming case to fix it. --- .../exoplayer/dash/mpd/SegmentBase.java | 4 +- .../SmoothStreamingManifest.java | 39 +++----------- .../google/android/exoplayer/util/Util.java | 53 +++++++++++++++++++ 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index 89a9dd49be..df92d029bc 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.dash.mpd; +import com.google.android.exoplayer.util.Util; + import android.net.Uri; import java.util.List; @@ -155,7 +157,7 @@ public abstract class SegmentBase { } else { unscaledSegmentTime = (sequenceNumber - startNumber) * duration; } - return (unscaledSegmentTime * 1000000) / timescale; + return Util.scaleLargeTimestamp(unscaledSegmentTime, 1000000, timescale); } public abstract RangedUri getSegmentUrl(Representation representation, int index); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index a26ca6a48e..7b45aed9cc 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -53,19 +53,8 @@ public class SmoothStreamingManifest { this.isLive = isLive; this.protectionElement = protectionElement; this.streamElements = streamElements; - if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) { - long divisionFactor = timescale / MICROS_PER_SECOND; - dvrWindowLengthUs = dvrWindowLength / divisionFactor; - durationUs = duration / divisionFactor; - } else if (timescale < MICROS_PER_SECOND && (MICROS_PER_SECOND % timescale) == 0) { - long multiplicationFactor = MICROS_PER_SECOND / timescale; - dvrWindowLengthUs = dvrWindowLength * multiplicationFactor; - durationUs = duration * multiplicationFactor; - } else { - double multiplicationFactor = (double) MICROS_PER_SECOND / timescale; - dvrWindowLengthUs = (long) (dvrWindowLength * multiplicationFactor); - durationUs = (long) (duration * multiplicationFactor); - } + dvrWindowLengthUs = Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale); + durationUs = Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale); } /** @@ -186,26 +175,10 @@ public class SmoothStreamingManifest { this.tracks = tracks; this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; - chunkStartTimesUs = new long[chunkStartTimes.size()]; - if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) { - long divisionFactor = timescale / MICROS_PER_SECOND; - for (int i = 0; i < chunkStartTimesUs.length; i++) { - chunkStartTimesUs[i] = chunkStartTimes.get(i) / divisionFactor; - } - lastChunkDurationUs = lastChunkDuration / divisionFactor; - } else if (timescale < MICROS_PER_SECOND && (MICROS_PER_SECOND % timescale) == 0) { - long multiplicationFactor = MICROS_PER_SECOND / timescale; - for (int i = 0; i < chunkStartTimesUs.length; i++) { - chunkStartTimesUs[i] = chunkStartTimes.get(i) * multiplicationFactor; - } - lastChunkDurationUs = lastChunkDuration * multiplicationFactor; - } else { - double multiplicationFactor = (double) MICROS_PER_SECOND / timescale; - for (int i = 0; i < chunkStartTimesUs.length; i++) { - chunkStartTimesUs[i] = (long) (chunkStartTimes.get(i) * multiplicationFactor); - } - lastChunkDurationUs = (long) (lastChunkDuration * multiplicationFactor); - } + lastChunkDurationUs = + Util.scaleLargeTimestamp(lastChunkDuration, MICROS_PER_SECOND, timescale); + chunkStartTimesUs = + Util.scaleLargeTimestamps(chunkStartTimes, MICROS_PER_SECOND, timescale); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 4c08c5a528..b8cd40215d 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -346,4 +346,57 @@ public final class Util { return time; } + /** + * Scales a large timestamp. + *

+ * Logically, scaling consists of a multiplication followed by a division. The actual operations + * performed are designed to minimize the probability of overflow. + * + * @param timestamp The timestamp to scale. + * @param multiplier The multiplier. + * @param divisor The divisor. + * @return The scaled timestamp. + */ + public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) { + if (divisor >= multiplier && (divisor % multiplier) == 0) { + long divisionFactor = divisor / multiplier; + return timestamp / divisionFactor; + } else if (divisor < multiplier && (multiplier % divisor) == 0) { + long multiplicationFactor = multiplier / divisor; + return timestamp * multiplicationFactor; + } else { + double multiplicationFactor = (double) multiplier / divisor; + return (long) (timestamp * multiplicationFactor); + } + } + + /** + * Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps. + * + * @param timestamps The timestamps to scale. + * @param multiplier The multiplier. + * @param divisor The divisor. + * @return The scaled timestamps. + */ + public static long[] scaleLargeTimestamps(List timestamps, long multiplier, long divisor) { + long[] scaledTimestamps = new long[timestamps.size()]; + if (divisor >= multiplier && (divisor % multiplier) == 0) { + long divisionFactor = divisor / multiplier; + for (int i = 0; i < scaledTimestamps.length; i++) { + scaledTimestamps[i] = timestamps.get(i) / divisionFactor; + } + } else if (divisor < multiplier && (multiplier % divisor) == 0) { + long multiplicationFactor = multiplier / divisor; + for (int i = 0; i < scaledTimestamps.length; i++) { + scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor; + } + } else { + double multiplicationFactor = (double) multiplier / divisor; + for (int i = 0; i < scaledTimestamps.length; i++) { + scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor); + } + } + return scaledTimestamps; + } + } From 165562d8808cc46578d1732f76112c732c53662e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:15:16 +0000 Subject: [PATCH 096/110] Add VSYNC aligning smooth frame release helper. --- .../SmoothFrameReleaseTimeHelper.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java diff --git a/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java b/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java new file mode 100644 index 0000000000..7248f1cdeb --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java @@ -0,0 +1,180 @@ +/* + * 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; + +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer.FrameReleaseTimeHelper; + +import android.annotation.TargetApi; +import android.view.Choreographer; +import android.view.Choreographer.FrameCallback; + +/** + * Makes a best effort to adjust frame release timestamps for a smoother visual result. + */ +@TargetApi(16) +public class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelper, FrameCallback { + + private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; + private static final long MAX_ALLOWED_DRIFT_NS = 20000000; + + private static final long VSYNC_OFFSET_PERCENTAGE = 80; + private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; + + private final boolean usePrimaryDisplayVsync; + private final long vsyncDurationNs; + private final long vsyncOffsetNs; + + private Choreographer choreographer; + private long sampledVsyncTimeNs; + + private long lastUnadjustedFrameTimeUs; + private long adjustedLastFrameTimeNs; + private long pendingAdjustedFrameTimeNs; + + private boolean haveSync; + private long syncReleaseTimeNs; + private long syncFrameTimeNs; + private int frameCount; + + /** + * @param primaryDisplayRefreshRate The refresh rate of the default display. + * @param usePrimaryDisplayVsync Whether to snap to the primary display vsync. May not be + * suitable when rendering to secondary displays. + */ + public SmoothFrameReleaseTimeHelper( + float primaryDisplayRefreshRate, boolean usePrimaryDisplayVsync) { + this.usePrimaryDisplayVsync = usePrimaryDisplayVsync; + if (usePrimaryDisplayVsync) { + vsyncDurationNs = (long) (1000000000d / primaryDisplayRefreshRate); + vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; + } else { + vsyncDurationNs = -1; + vsyncOffsetNs = -1; + } + } + + @Override + public void enable() { + haveSync = false; + if (usePrimaryDisplayVsync) { + sampledVsyncTimeNs = 0; + choreographer = Choreographer.getInstance(); + choreographer.postFrameCallback(this); + } + } + + @Override + public void disable() { + if (usePrimaryDisplayVsync) { + choreographer.removeFrameCallback(this); + choreographer = null; + } + } + + @Override + public void doFrame(long vsyncTimeNs) { + sampledVsyncTimeNs = vsyncTimeNs; + choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS); + } + + @Override + public long adjustReleaseTime(long unadjustedFrameTimeUs, long unadjustedReleaseTimeNs) { + long unadjustedFrameTimeNs = unadjustedFrameTimeUs * 1000; + + // Until we know better, the adjustment will be a no-op. + long adjustedFrameTimeNs = unadjustedFrameTimeNs; + long adjustedReleaseTimeNs = unadjustedReleaseTimeNs; + + if (haveSync) { + // See if we've advanced to the next frame. + if (unadjustedFrameTimeUs != lastUnadjustedFrameTimeUs) { + frameCount++; + adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs; + } + if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) { + // We're synced and have waited the required number of frames to apply an adjustment. + // Calculate the average frame time across all the frames we've seen since the last sync. + // This will typically give us a framerate at a finer granularity than the frame times + // themselves (which often only have millisecond granularity). + long averageFrameTimeNs = (unadjustedFrameTimeNs - syncFrameTimeNs) / frameCount; + // Project the adjusted frame time forward using the average. + long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameTimeNs; + + if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) { + haveSync = false; + } else { + adjustedFrameTimeNs = candidateAdjustedFrameTimeNs; + adjustedReleaseTimeNs = syncReleaseTimeNs + adjustedFrameTimeNs - syncFrameTimeNs; + } + } else { + // We're synced but haven't waited the required number of frames to apply an adjustment. + // Check drift anyway. + if (isDriftTooLarge(unadjustedFrameTimeNs, unadjustedReleaseTimeNs)) { + haveSync = false; + } + } + } + + // If we need to sync, do so now. + if (!haveSync) { + syncFrameTimeNs = unadjustedFrameTimeNs; + syncReleaseTimeNs = unadjustedReleaseTimeNs; + frameCount = 0; + haveSync = true; + onSynced(); + } + + lastUnadjustedFrameTimeUs = unadjustedFrameTimeUs; + pendingAdjustedFrameTimeNs = adjustedFrameTimeNs; + + if (sampledVsyncTimeNs == 0) { + return adjustedReleaseTimeNs; + } + + // Find the timestamp of the closest vsync. This is the vsync that we're targeting. + long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs); + // Apply an offset so that we release before the target vsync, but after the previous one. + return snappedTimeNs - vsyncOffsetNs; + } + + protected void onSynced() { + // Do nothing. + } + + private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) { + long elapsedFrameTimeNs = frameTimeNs - syncFrameTimeNs; + long elapsedReleaseTimeNs = releaseTimeNs - syncReleaseTimeNs; + return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS; + } + + private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) { + long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration; + long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount); + long snappedBeforeNs; + long snappedAfterNs; + if (releaseTime <= snappedTimeNs) { + snappedBeforeNs = snappedTimeNs - vsyncDuration; + snappedAfterNs = snappedTimeNs; + } else { + snappedBeforeNs = snappedTimeNs; + snappedAfterNs = snappedTimeNs + vsyncDuration; + } + long snappedAfterDiff = snappedAfterNs - releaseTime; + long snappedBeforeDiff = releaseTime - snappedBeforeNs; + return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; + } + +} From 656fc0b0ca4b2ba5269227848a5da9294d4854f5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:26:48 +0000 Subject: [PATCH 097/110] Make sure SmoothStreaming manifest durations are -1 for Live. Plus start to properly document the SmoothStreaming package. Note that where the documentation is a little vague, this is because the original SmoothStreaming documentation is equally vague! --- .../java/com/google/android/exoplayer/C.java | 5 ++ .../exoplayer/FrameworkSampleSource.java | 6 +- .../google/android/exoplayer/TrackInfo.java | 12 ++++ .../android/exoplayer/TrackRenderer.java | 4 +- .../chunk/SingleSampleChunkSource.java | 4 +- .../SmoothStreamingManifest.java | 58 +++++++++++++++++-- 6 files changed, 79 insertions(+), 10 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index f0f2a57fec..b852676bd7 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -20,6 +20,11 @@ package com.google.android.exoplayer; */ public final class C { + /** + * Represents an unknown microsecond time or duration. + */ + public static final long UNKNOWN_TIME_US = -1; + /** * Represents an unbounded length of data. */ 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 0fc39b0e1a..716ef7dafb 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -71,10 +71,10 @@ public final class FrameworkSampleSource implements SampleSource { trackInfos = new TrackInfo[trackStates.length]; for (int i = 0; i < trackStates.length; i++) { android.media.MediaFormat format = extractor.getTrackFormat(i); - long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? - format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME_US; + long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? + format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; String mime = format.getString(android.media.MediaFormat.KEY_MIME); - trackInfos[i] = new TrackInfo(mime, duration); + trackInfos[i] = new TrackInfo(mime, durationUs); } prepared = true; } diff --git a/library/src/main/java/com/google/android/exoplayer/TrackInfo.java b/library/src/main/java/com/google/android/exoplayer/TrackInfo.java index e6c1b0c977..72487a0cdf 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackInfo.java @@ -20,9 +20,21 @@ package com.google.android.exoplayer; */ public final class TrackInfo { + /** + * The mime type. + */ public final String mimeType; + + /** + * The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown. + */ public final long durationUs; + /** + * @param mimeType The mime type. + * @param durationUs The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration + * is unknown. + */ public TrackInfo(String mimeType, long durationUs) { this.mimeType = mimeType; this.durationUs = durationUs; 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 66e20291f7..cf4f8f13fc 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -67,9 +67,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent { protected static final int STATE_STARTED = 3; /** - * Represents an unknown time or duration. + * Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}. */ - public static final long UNKNOWN_TIME_US = -1; + public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1 /** * Represents a time or duration that should match the duration of the longest track whose * duration is known. diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java index ffb90eaefd..71a50241f4 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.chunk; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.upstream.DataSource; @@ -42,7 +43,8 @@ public class SingleSampleChunkSource implements ChunkSource { * @param dataSource A {@link DataSource} suitable for loading the sample data. * @param dataSpec Defines the location of the sample. * @param format The format of the sample. - * @param durationUs The duration of the sample in microseconds. + * @param durationUs The duration of the sample in microseconds, or {@link C#UNKNOWN_TIME_US} if + * the duration is unknown. * @param mediaFormat The sample media format. May be null. */ public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format, diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 7b45aed9cc..8e4a0cada0 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.smoothstreaming; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; @@ -33,28 +34,77 @@ public class SmoothStreamingManifest { private static final long MICROS_PER_SECOND = 1000000L; + /** + * The client manifest major version. + */ public final int majorVersion; + + /** + * The client manifest minor version. + */ public final int minorVersion; - public final long timescale; + + /** + * The number of fragments in a lookahead, or -1 if the lookahead is unspecified. + */ public final int lookAheadCount; + + /** + * True if the manifest describes a live presentation still in progress. False otherwise. + */ public final boolean isLive; + + /** + * Content protection information, or null if the content is not protected. + */ public final ProtectionElement protectionElement; + + /** + * The contained stream elements. + */ public final StreamElement[] streamElements; + + /** + * The overall presentation duration of the media in microseconds, or {@link C#UNKNOWN_TIME_US} + * if the duration is unknown. + */ public final long durationUs; + + /** + * The length of the trailing window for a live broadcast in microseconds, or + * {@link C#UNKNOWN_TIME_US} if the stream is not live or if the window length is unspecified. + */ public final long dvrWindowLengthUs; + /** + * @param majorVersion The client manifest major version. + * @param minorVersion The client manifest minor version. + * @param timescale The timescale of the media as the number of units that pass in one second. + * @param duration The overall presentation duration in units of the timescale attribute, or 0 + * if the duration is unknown. + * @param dvrWindowLength The length of the trailing window in units of the timescale attribute, + * or 0 if this attribute is unspecified or not applicable. + * @param lookAheadCount The number of fragments in a lookahead, or -1 if this attribute is + * unspecified or not applicable. + * @param isLive True if the manifest describes a live presentation still in progress. False + * otherwise. + * @param protectionElement Content protection information, or null if the content is not + * protected. + * @param streamElements The contained stream elements. + */ public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration, long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, StreamElement[] streamElements) { this.majorVersion = majorVersion; this.minorVersion = minorVersion; - this.timescale = timescale; this.lookAheadCount = lookAheadCount; this.isLive = isLive; this.protectionElement = protectionElement; this.streamElements = streamElements; - dvrWindowLengthUs = Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale); - durationUs = Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale); + dvrWindowLengthUs = dvrWindowLength == 0 ? C.UNKNOWN_TIME_US + : Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale); + durationUs = duration == 0 ? C.UNKNOWN_TIME_US + : Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale); } /** From dc644ae86d3aeaf15c53aac50faaddacd193af22 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:30:15 +0000 Subject: [PATCH 098/110] Make single MICROS_PER_SECOND constant + use it everywhere. --- .../src/main/java/com/google/android/exoplayer/C.java | 7 ++++++- .../google/android/exoplayer/audio/AudioTrack.java | 11 +++++------ .../android/exoplayer/dash/mpd/SegmentBase.java | 10 ++++++---- .../exoplayer/parser/mp4/FragmentedMp4Extractor.java | 9 ++++++--- .../smoothstreaming/SmoothStreamingManifest.java | 10 ++++------ .../android/exoplayer/text/ttml/TtmlParser.java | 5 +++-- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index b852676bd7..f710b0d7a7 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -23,7 +23,12 @@ public final class C { /** * Represents an unknown microsecond time or duration. */ - public static final long UNKNOWN_TIME_US = -1; + public static final long UNKNOWN_TIME_US = -1L; + + /** + * The number of microseconds in one second. + */ + public static final long MICROS_PER_SECOND = 1000000L; /** * Represents an unbounded length of data. diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index ac49449eac..4f32d9a109 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.audio; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; @@ -80,22 +81,20 @@ public final class AudioTrack { private static final String TAG = "AudioTrack"; - private static final long MICROS_PER_SECOND = 1000000L; - /** * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more * than this amount. * *

This is a fail safe that should not be required on correctly functioning devices. */ - private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * C.MICROS_PER_SECOND; /** * AudioTrack latencies are deemed impossibly large if they are greater than this amount. * *

This is a fail safe that should not be required on correctly functioning devices. */ - private static final long MAX_LATENCY_US = 10 * MICROS_PER_SECOND; + private static final long MAX_LATENCY_US = 10 * C.MICROS_PER_SECOND; private static final int START_NOT_SET = 0; private static final int START_IN_SYNC = 1; @@ -624,11 +623,11 @@ public final class AudioTrack { } private long framesToDurationUs(long frameCount) { - return (frameCount * MICROS_PER_SECOND) / sampleRate; + return (frameCount * C.MICROS_PER_SECOND) / sampleRate; } private long durationUsToFrames(long durationUs) { - return (durationUs * sampleRate) / MICROS_PER_SECOND; + return (durationUs * sampleRate) / C.MICROS_PER_SECOND; } private void resetSyncParams() { diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index df92d029bc..a7393865f7b 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.dash.mpd; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -141,11 +142,12 @@ public abstract class SegmentBase { public final long getSegmentDurationUs(int sequenceNumber) { if (segmentTimeline != null) { - return (segmentTimeline.get(sequenceNumber - startNumber).duration * 1000000) / timescale; + long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; + return (duration * C.MICROS_PER_SECOND) / timescale; } else { return sequenceNumber == getLastSegmentNum() - ? (periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber) - : ((duration * 1000000L) / timescale); + ? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber)) + : ((duration * C.MICROS_PER_SECOND) / timescale); } } @@ -157,7 +159,7 @@ public abstract class SegmentBase { } else { unscaledSegmentTime = (sequenceNumber - startNumber) * duration; } - return Util.scaleLargeTimestamp(unscaledSegmentTime, 1000000, timescale); + return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale); } public abstract RangedUri getSegmentUrl(Representation representation, int index); 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 ad5308d7c0..c9d2caf953 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.parser.mp4; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; @@ -26,6 +27,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; import android.media.MediaCodec; @@ -1053,6 +1055,7 @@ public final class FragmentedMp4Extractor implements Extractor { long offset = firstOffset; long time = earliestPresentationTime; + long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); for (int i = 0; i < referenceCount; i++) { int firstInt = atom.readInt(); @@ -1067,10 +1070,10 @@ public final class FragmentedMp4Extractor implements Extractor { // Calculate time and duration values such that any rounding errors are consistent. i.e. That // timesUs[i] + durationsUs[i] == timesUs[i + 1]. - timesUs[i] = (time * 1000000L) / timescale; - long nextTimeUs = ((time + referenceDuration) * 1000000L) / timescale; - durationsUs[i] = nextTimeUs - timesUs[i]; + timesUs[i] = timeUs; time += referenceDuration; + timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); + durationsUs[i] = timeUs - timesUs[i]; atom.skip(4); offset += sizes[i]; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 8e4a0cada0..271949b58b 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -32,8 +32,6 @@ import java.util.UUID; */ public class SmoothStreamingManifest { - private static final long MICROS_PER_SECOND = 1000000L; - /** * The client manifest major version. */ @@ -102,9 +100,9 @@ public class SmoothStreamingManifest { this.protectionElement = protectionElement; this.streamElements = streamElements; dvrWindowLengthUs = dvrWindowLength == 0 ? C.UNKNOWN_TIME_US - : Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale); + : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale); durationUs = duration == 0 ? C.UNKNOWN_TIME_US - : Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale); + : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale); } /** @@ -226,9 +224,9 @@ public class SmoothStreamingManifest { this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; lastChunkDurationUs = - Util.scaleLargeTimestamp(lastChunkDuration, MICROS_PER_SECOND, timescale); + Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale); chunkStartTimesUs = - Util.scaleLargeTimestamps(chunkStartTimes, MICROS_PER_SECOND, timescale); + Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index 82b41d3d8b..758a0e9b38 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.text.ttml; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.SubtitleParser; @@ -254,7 +255,7 @@ public class TtmlParser implements SubtitleParser { String subframes = matcher.group(6); durationSeconds += (subframes != null) ? ((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0; - return (long) (durationSeconds * 1000000); + return (long) (durationSeconds * C.MICROS_PER_SECOND); } matcher = OFFSET_TIME.matcher(time); if (matcher.matches()) { @@ -274,7 +275,7 @@ public class TtmlParser implements SubtitleParser { } else if (unit.equals("t")) { offsetSeconds /= tickRate; } - return (long) (offsetSeconds * 1000000); + return (long) (offsetSeconds * C.MICROS_PER_SECOND); } throw new ParserException("Malformed time expression: " + time); } From 2e1f9897e76005978444eb617a862c1cdfa6b5c1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:30:56 +0000 Subject: [PATCH 099/110] Fixed issue in which setting a representation duration to unknown wasn't handled correctly. --- .../com/google/android/exoplayer/dash/DashChunkSource.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 395d9ba64d..f663457ce7 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -72,14 +72,16 @@ public class DashChunkSource implements ChunkSource { */ public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { + long periodDurationUs = (representations[0].periodDurationMs == -1) + ? -1 : representations[0].periodDurationMs * 1000; + this.dataSource = dataSource; this.evaluator = evaluator; this.formats = new Format[representations.length]; 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, periodDurationUs); this.evaluation = new Evaluation(); int maxWidth = 0; int maxHeight = 0; From 6652f864bd69f04b1793eaeadc63edf80ba2571b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:33:36 +0000 Subject: [PATCH 100/110] Audio improvements. --- .../audio/AudioCapabilitiesReceiver.java | 2 +- .../android/exoplayer/audio/AudioTrack.java | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java index 963bb344a8..18e8d2a281 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java @@ -40,7 +40,7 @@ public final class AudioCapabilitiesReceiver { } - /** Default to stereo PCM on SDK <= 21 and when HDMI is unplugged. */ + /** Default to stereo PCM on SDK < 21 and when HDMI is unplugged. */ private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2); diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 4f32d9a109..405f3e9c6b 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioManager; @@ -96,6 +97,9 @@ public final class AudioTrack { */ private static final long MAX_LATENCY_US = 10 * C.MICROS_PER_SECOND; + /** Value for ac3Bitrate before the bitrate has been calculated. */ + private static final int UNKNOWN_AC3_BITRATE = 0; + private static final int START_NOT_SET = 0; private static final int START_IN_SYNC = 1; private static final int START_NEED_SYNC = 2; @@ -138,6 +142,11 @@ public final class AudioTrack { private int temporaryBufferOffset; private int temporaryBufferSize; + private boolean isAc3; + + /** Bitrate measured in kilobits per second, if {@link #isAc3} is true. */ + private int ac3Bitrate; + /** Constructs an audio track using the default minimum buffer size multiplier. */ public AudioTrack() { this(DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR); @@ -276,6 +285,7 @@ public final class AudioTrack { * @param bufferSize The total size of the playback buffer in bytes. Specify 0 to use a buffer * size based on the minimum for format. */ + @SuppressLint("InlinedApi") public void reconfigure(MediaFormat format, int encoding, int bufferSize) { int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelConfig; @@ -299,8 +309,9 @@ public final class AudioTrack { int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); // TODO: Does channelConfig determine channelCount? + boolean isAc3 = encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3; if (audioTrack != null && this.sampleRate == sampleRate - && this.channelConfig == channelConfig) { + && this.channelConfig == channelConfig && !this.isAc3 && !isAc3) { // We already have an existing audio track with the correct sample rate and channel config. return; } @@ -314,7 +325,8 @@ public final class AudioTrack { bufferSize == 0 ? (int) (minBufferMultiplicationFactor * minBufferSize) : bufferSize; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - + this.isAc3 = isAc3; + ac3Bitrate = UNKNOWN_AC3_BITRATE; // Calculated on receiving the first buffer if isAc3 is true. frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels. } @@ -352,6 +364,14 @@ public final class AudioTrack { int result = 0; if (temporaryBufferSize == 0 && size != 0) { + if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) { + // Each AC-3 buffer contains 1536 frames of audio, so the AudioTrack playback position + // advances by 1536 per buffer (32 ms at 48 kHz). Calculate the bitrate in kbit/s. + int unscaledAc3Bitrate = size * 8 * sampleRate; + int divisor = 1000 * 1536; + ac3Bitrate = (unscaledAc3Bitrate + divisor / 2) / divisor; + } + // This is the first time we've seen this {@code buffer}. // Note: presentationTimeUs corresponds to the end of the sample, not the start. long bufferStartTime = presentationTimeUs - framesToDurationUs(bytesToFrames(size)); @@ -615,11 +635,16 @@ public final class AudioTrack { } private long framesToBytes(long frameCount) { + // This method is unused on SDK >= 21. return frameCount * frameSize; } private long bytesToFrames(long byteCount) { - return byteCount / frameSize; + if (isAc3) { + return byteCount * 8 * sampleRate / (1000 * ac3Bitrate); + } else { + return byteCount / frameSize; + } } private long framesToDurationUs(long frameCount) { From 4efc0abde92a07c91d54c55ac32f821aec004eb7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:45:13 +0000 Subject: [PATCH 101/110] Implement DASH Live. Note: This adds support for the majority of DASH live streams, however we do not yet correctly support live streams that rely on UtcTimingElements in their manifests. Issue: #52 --- .../android/exoplayer/demo/DemoUtil.java | 2 +- .../android/exoplayer/demo/Samples.java | 24 +- .../demo/full/FullPlayerActivity.java | 6 +- ...rBuilder.java => DashRendererBuilder.java} | 193 ++++++---- .../SmoothStreamingRendererBuilder.java | 9 +- ...rBuilder.java => DashRendererBuilder.java} | 77 ++-- .../demo/simple/SimplePlayerActivity.java | 12 +- .../SmoothStreamingRendererBuilder.java | 6 +- .../chunk/MultiTrackChunkSource.java | 10 + .../exoplayer/dash/DashChunkSource.java | 346 +++++++++++++++--- .../android/exoplayer/dash/mpd/Period.java | 17 + .../exoplayer/util/ManifestFetcher.java | 77 +++- .../android/exoplayer/util/PlayerControl.java | 11 +- .../google/android/exoplayer/util/Util.java | 18 + 14 files changed, 609 insertions(+), 199 deletions(-) rename demo/src/main/java/com/google/android/exoplayer/demo/full/player/{DashVodRendererBuilder.java => DashRendererBuilder.java} (57%) rename demo/src/main/java/com/google/android/exoplayer/demo/simple/{DashVodRendererBuilder.java => DashRendererBuilder.java} (62%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index 880ecc3286..a55e2c2cb0 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -47,7 +47,7 @@ public class DemoUtil { public static final String CONTENT_TYPE_EXTRA = "content_type"; public static final String CONTENT_ID_EXTRA = "content_id"; - public static final int TYPE_DASH_VOD = 0; + public static final int TYPE_DASH = 0; public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 27d71a7d55..deea767d07 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -46,13 +46,13 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." - + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false, false), new Sample("Google Play (DASH)", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." - + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false, false), new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", @@ -66,13 +66,13 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." - + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false, true), new Sample("Google Play", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." - + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false, true), }; @@ -81,12 +81,12 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=A3EC7EE53ABE601B357F7CAB8B54AD0702CA85A7." - + "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true), + + "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, false, true), new Sample("Google Play", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=B752B262C6D7262EC4E4EB67901E5D8F7058A81D." - + "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true), + + "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, false, true), }; public static final Sample[] SMOOTHSTREAMING = new Sample[] { @@ -103,32 +103,32 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=41EA40A027A125A16292E0A5E3277A3B5FA9B938." - + "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: HDCP not required", "48fcc369939ac96c", "http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=315911BDCEED0FB0C763455BDCC97449DAAFA9E8." - + "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: HDCP required", "e06c39f1151da3df", "http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=A47A1E13E7243BD567601A75F79B34644D0DC592." - + "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: Secure video path required", "0894c7c8719b28a0", "http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=2847EE498970F6B45176766CD2802FEB4D4CB7B2." - + "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a", "http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F." - + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: 30s license duration", "f9a34cab7b05881a", "http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." - + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true, true), }; public static final Sample[] MISC = new Sample[] { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index c1fe79eb0f..423af3d40e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.VideoSurfaceView; import com.google.android.exoplayer.demo.DemoUtil; import com.google.android.exoplayer.demo.R; -import com.google.android.exoplayer.demo.full.player.DashVodRendererBuilder; +import com.google.android.exoplayer.demo.full.player.DashRendererBuilder; import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; @@ -172,8 +172,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, new SmoothStreamingTestMediaDrmCallback(), debugTextView); - case DemoUtil.TYPE_DASH_VOD: - return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, + case DemoUtil.TYPE_DASH: + return new DashRendererBuilder(userAgent, contentUri.toString(), contentId, new WidevineTestMediaDrmCallback(contentId), debugTextView); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java similarity index 57% rename from demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index 9ce6613fa2..f998d6e30f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java @@ -40,6 +40,8 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderC import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.text.webvtt.WebvttParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; @@ -58,16 +60,19 @@ import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** - * A {@link RendererBuilder} for DASH VOD. + * A {@link RendererBuilder} for DASH. */ -public class DashVodRendererBuilder implements RendererBuilder, +public class DashRendererBuilder implements RendererBuilder, ManifestCallback { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; + private static final int TEXT_BUFFER_SEGMENTS = 2; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private static final int SECURITY_LEVEL_UNKNOWN = -1; private static final int SECURITY_LEVEL_1 = 1; @@ -81,8 +86,9 @@ public class DashVodRendererBuilder implements RendererBuilder, private DemoPlayer player; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; - public DashVodRendererBuilder(String userAgent, String url, String contentId, + public DashRendererBuilder(String userAgent, String url, String contentId, MediaDrmCallback drmCallback, TextView debugTextView) { this.userAgent = userAgent; this.url = url; @@ -96,8 +102,8 @@ public class DashVodRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); + manifestFetcher = new ManifestFetcher(parser, contentId, url, + userAgent); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -108,38 +114,17 @@ public class DashVodRendererBuilder implements RendererBuilder, @Override public void onManifest(String contentId, MediaPresentationDescription manifest) { + Period period = manifest.periods.get(0); Handler mainHandler = player.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); - // Obtain Representations for playback. - int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); - ArrayList audioRepresentationsList = new ArrayList(); - ArrayList videoRepresentationsList = new ArrayList(); - Period period = manifest.periods.get(0); - boolean hasContentProtection = false; - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - hasContentProtection |= adaptationSet.hasContentProtection(); - int adaptationSetType = adaptationSet.type; - for (int j = 0; j < adaptationSet.representations.size(); j++) { - Representation representation = adaptationSet.representations.get(j); - if (adaptationSetType == AdaptationSet.TYPE_AUDIO) { - audioRepresentationsList.add(representation); - } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { - Format format = representation.format; - if (format.width * format.height <= maxDecodableFrameSize) { - videoRepresentationsList.add(representation); - } else { - // The device isn't capable of playing this stream. - } - } - } - } - Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; - videoRepresentationsList.toArray(videoRepresentations); + int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO); + AdaptationSet videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex); // Check drm support if necessary. + boolean hasContentProtection = videoAdaptationSet.hasContentProtection(); + boolean filterHdContent = false; DrmSessionManager drmSessionManager = null; if (hasContentProtection) { if (Util.SDK_INT < 18) { @@ -151,55 +136,81 @@ public class DashVodRendererBuilder implements RendererBuilder, Pair drmSessionManagerData = V18Compat.getDrmSessionManagerData(player, drmCallback); drmSessionManager = drmSessionManagerData.first; - if (!drmSessionManagerData.second) { - // HD streams require L1 security. - videoRepresentations = getSdRepresentations(videoRepresentations); - } + // HD streams require L1 security. + filterHdContent = !drmSessionManagerData.second; } catch (Exception e) { callback.onRenderersError(e); return; } } - // Build the video renderer. - DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource videoChunkSource; - String mimeType = videoRepresentations[0].format.mimeType; - if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) { - videoChunkSource = new DashChunkSource(videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); - } else { - throw new IllegalStateException("Unexpected mime type: " + mimeType); + // Determine which video representations we should use for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + List videoRepresentations = videoAdaptationSet.representations; + ArrayList videoRepresentationIndexList = new ArrayList(); + for (int i = 0; i < videoRepresentations.size(); i++) { + Format format = videoRepresentations.get(i).format; + if (filterHdContent && (format.width >= 1280 || format.height >= 720)) { + // Filtering HD content + } else if (format.width * format.height > maxDecodableFrameSize) { + // Filtering stream that device cannot play + } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) + && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { + // Filtering unsupported mime type + } else { + videoRepresentationIndexList.add(i); + } + } + + // Build the video renderer. + final MediaCodecVideoTrackRenderer videoRenderer; + final TrackRenderer debugRenderer; + if (videoRepresentationIndexList.isEmpty()) { + videoRenderer = null; + debugRenderer = null; + } else { + int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, + videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), + LIVE_EDGE_LATENCY_MS); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + DemoPlayer.TYPE_VIDEO); + videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); + debugRenderer = debugTextView != null + ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null; + } + + // Build the audio chunk sources. + int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO); + AdaptationSet audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex); + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); + FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); + List audioChunkSourceList = new ArrayList(); + List audioTrackNameList = new ArrayList(); + List audioRepresentations = audioAdaptationSet.representations; + for (int i = 0; i < audioRepresentations.size(); i++) { + Format format = audioRepresentations.get(i).format; + audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " + + format.audioSamplingRate + "Hz)"); + audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, + new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS)); } - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, - DemoPlayer.TYPE_VIDEO); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, - mainHandler, player, 50); // Build the audio renderer. final String[] audioTrackNames; final MultiTrackChunkSource audioChunkSource; final TrackRenderer audioRenderer; - if (audioRepresentationsList.isEmpty()) { + if (audioChunkSourceList.isEmpty()) { audioTrackNames = null; audioChunkSource = null; audioRenderer = null; } else { - DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); - audioTrackNames = new String[audioRepresentationsList.size()]; - ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; - FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); - for (int i = 0; i < audioRepresentationsList.size(); i++) { - Representation representation = audioRepresentationsList.get(i); - Format format = representation.format; - audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " + - format.audioSamplingRate + "Hz)"; - audioChunkSources[i] = new DashChunkSource(audioDataSource, - audioEvaluator, representation); - } - audioChunkSource = new MultiTrackChunkSource(audioChunkSources); + audioTrackNames = new String[audioTrackNameList.size()]; + audioTrackNameList.toArray(audioTrackNames); + audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_AUDIO); @@ -207,37 +218,61 @@ public class DashVodRendererBuilder implements RendererBuilder, mainHandler, player); } - // Build the debug renderer. - TrackRenderer debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null; + // Build the text chunk sources. + DataSource textDataSource = new UriDataSource(userAgent, bandwidthMeter); + FormatEvaluator textEvaluator = new FormatEvaluator.FixedEvaluator(); + List textChunkSourceList = new ArrayList(); + List textTrackNameList = new ArrayList(); + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + if (adaptationSet.type == AdaptationSet.TYPE_TEXT) { + List representations = adaptationSet.representations; + for (int j = 0; j < representations.size(); j++) { + Representation representation = representations.get(j); + textTrackNameList.add(representation.format.id); + textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j}, + textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS)); + } + } + } + + // Build the text renderers + final String[] textTrackNames; + final MultiTrackChunkSource textChunkSource; + final TrackRenderer textRenderer; + if (textChunkSourceList.isEmpty()) { + textTrackNames = null; + textChunkSource = null; + textRenderer = null; + } else { + textTrackNames = new String[textTrackNameList.size()]; + textTrackNameList.toArray(textTrackNames); + textChunkSource = new MultiTrackChunkSource(textChunkSourceList); + SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + DemoPlayer.TYPE_TEXT); + textRenderer = new TextTrackRenderer(textSampleSource, new WebvttParser(), player, + mainHandler.getLooper()); + } // Invoke the callback. String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][]; trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames; + trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames; MultiTrackChunkSource[] multiTrackChunkSources = new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; + multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource; TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_TEXT] = textRenderer; renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; callback.onRenderers(trackNames, multiTrackChunkSources, renderers); } - private Representation[] getSdRepresentations(Representation[] representations) { - ArrayList sdRepresentations = new ArrayList(); - for (int i = 0; i < representations.length; i++) { - if (representations[i].format.height < 720 && representations[i].format.width < 1280) { - sdRepresentations.add(representations[i]); - } - } - Representation[] sdRepresentationArray = new Representation[sdRepresentations.size()]; - sdRepresentations.toArray(sdRepresentationArray); - return sdRepresentationArray; - } - @TargetApi(18) private static class V18Compat { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 2fb473239f..7d88519b45 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -64,7 +64,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; - private static final int TTML_BUFFER_SEGMENTS = 2; + private static final int TEXT_BUFFER_SEGMENTS = 2; private static final int LIVE_EDGE_LATENCY_MS = 30000; private final String userAgent; @@ -149,10 +149,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } } } - int[] videoTrackIndices = new int[videoTrackIndexList.size()]; - for (int i = 0; i < videoTrackIndexList.size(); i++) { - videoTrackIndices[i] = videoTrackIndexList.get(i); - } + int[] videoTrackIndices = Util.toArray(videoTrackIndexList); // Build the video renderer. DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); @@ -221,7 +218,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } textChunkSource = new MultiTrackChunkSource(textChunkSources); ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl, - TTML_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_TEXT); textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player, mainHandler.getLooper()); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashRendererBuilder.java similarity index 62% rename from demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/simple/DashRendererBuilder.java index 547ca0fefc..46923c6b74 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashRendererBuilder.java @@ -40,22 +40,26 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; import android.media.MediaCodec; import android.os.Handler; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** - * A {@link RendererBuilder} for DASH VOD. + * A {@link RendererBuilder} for DASH. */ -/* package */ class DashVodRendererBuilder implements RendererBuilder, +/* package */ class DashRendererBuilder implements RendererBuilder, ManifestCallback { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private final SimplePlayerActivity playerActivity; private final String userAgent; @@ -63,8 +67,9 @@ import java.util.ArrayList; private final String contentId; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; - public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, + public DashRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, String contentId) { this.playerActivity = playerActivity; this.userAgent = userAgent; @@ -76,8 +81,8 @@ import java.util.ArrayList; public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); + manifestFetcher = new ManifestFetcher(parser, contentId, url, + userAgent); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @@ -88,48 +93,50 @@ import java.util.ArrayList; @Override public void onManifest(String contentId, MediaPresentationDescription manifest) { + Period period = manifest.periods.get(0); Handler mainHandler = playerActivity.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - // Obtain Representations for playback. + // Determine which video representations we should use for playback. int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); - Representation audioRepresentation = null; - ArrayList videoRepresentationsList = new ArrayList(); - Period period = manifest.periods.get(0); - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - int adaptationSetType = adaptationSet.type; - for (int j = 0; j < adaptationSet.representations.size(); j++) { - Representation representation = adaptationSet.representations.get(j); - if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) { - audioRepresentation = representation; - } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { - Format format = representation.format; - if (format.width * format.height <= maxDecodableFrameSize) { - videoRepresentationsList.add(representation); - } else { - // The device isn't capable of playing this stream. - } - } + int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO); + List videoRepresentations = + period.adaptationSets.get(videoAdaptationSetIndex).representations; + ArrayList videoRepresentationIndexList = new ArrayList(); + for (int i = 0; i < videoRepresentations.size(); i++) { + Format format = videoRepresentations.get(i).format; + if (format.width * format.height > maxDecodableFrameSize) { + // Filtering stream that device cannot play + } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) + && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { + // Filtering unsupported mime type + } else { + videoRepresentationIndexList.add(i); } } - Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; - videoRepresentationsList.toArray(videoRepresentations); // Build the video renderer. - DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource videoChunkSource = new DashChunkSource(videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); + final MediaCodecVideoTrackRenderer videoRenderer; + if (videoRepresentationIndexList.isEmpty()) { + videoRenderer = null; + } else { + int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, + videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), + LIVE_EDGE_LATENCY_MS); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); + videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); + } // Build the audio renderer. + int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO); DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource audioChunkSource = new DashChunkSource(audioDataSource, - new FormatEvaluator.FixedEvaluator(), audioRepresentation); + ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, + new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index 1622998ae4..8c47dea3c1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -61,10 +61,6 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private static final String TAG = "PlayerActivity"; - public static final int TYPE_DASH_VOD = 0; - public static final int TYPE_SS_VOD = 1; - public static final int TYPE_OTHER = 2; - private MediaController mediaController; private Handler mainHandler; private View shutterView; @@ -90,7 +86,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call Intent intent = getIntent(); contentUri = intent.getData(); - contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER); + contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_OTHER); contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA); mainHandler = new Handler(getMainLooper()); @@ -165,11 +161,11 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private RendererBuilder getRendererBuilder() { String userAgent = DemoUtil.getUserAgent(this); switch (contentType) { - case TYPE_SS_VOD: + case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), contentId); - case TYPE_DASH_VOD: - return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId); + case DemoUtil.TYPE_DASH: + return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId); default: return new DefaultRendererBuilder(this, contentUri); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index 8686fa3b5a..90a06a6216 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.Util; import android.media.MediaCodec; import android.os.Handler; @@ -115,10 +116,7 @@ import java.util.ArrayList; } } } - int[] videoTrackIndices = new int[videoTrackIndexList.size()]; - for (int i = 0; i < videoTrackIndexList.size(); i++) { - videoTrackIndices[i] = videoTrackIndexList.get(i); - } + int[] videoTrackIndices = Util.toArray(videoTrackIndexList); // Build the video renderer. DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java index 2c7cf33649..ce9965f313 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java @@ -46,6 +46,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { this.selectedSource = sources[0]; } + public MultiTrackChunkSource(List sources) { + this(toChunkSourceArray(sources)); + } + /** * Gets the number of tracks that this source can switch between. May be called safely from any * thread. @@ -107,4 +111,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { selectedSource.onChunkLoadError(chunk, e); } + private static ChunkSource[] toChunkSourceArray(List sources) { + ChunkSource[] chunkSourceArray = new ChunkSource[sources.size()]; + sources.toArray(chunkSourceArray); + return chunkSourceArray; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index f663457ce7..9bcb1aa3b8 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer.dash; +import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.TrackInfo; +import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkSource; @@ -27,76 +29,175 @@ import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.Mp4MediaChunk; +import com.google.android.exoplayer.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.parser.Extractor; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.parser.webm.WebmExtractor; +import com.google.android.exoplayer.text.webvtt.WebvttParser; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; +import android.os.SystemClock; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; /** * An {@link ChunkSource} for DASH streams. *

- * This implementation currently supports fMP4 and webm. + * This implementation currently supports fMP4, webm, and webvtt. */ public class DashChunkSource implements ChunkSource { + /** + * Thrown when an AdaptationSet is missing from the MPD. + */ + public static class NoAdaptationSetException extends IOException { + + public NoAdaptationSetException(String message) { + super(message); + } + + } + + /** + * Specifies that we should process all tracks. + */ + public static final int USE_ALL_TRACKS = -1; + private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator evaluator; private final Evaluation evaluation; + private final StringBuilder headerBuilder; + private final long liveEdgeLatencyUs; private final int maxWidth; private final int maxHeight; private final Format[] formats; - private final HashMap representations; - private final HashMap extractors; - private final HashMap segmentIndexes; + private final HashMap representationHolders; + + private final ManifestFetcher manifestFetcher; + private final int adaptationSetIndex; + private final int[] representationIndices; + + private MediaPresentationDescription currentManifest; + private boolean finishedCurrentManifest; private boolean lastChunkWasInitialization; + private IOException fatalError; /** + * Lightweight constructor to use for fixed duration content. + * * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param evaluator Selects from the available formats. + * @param formatEvaluator Selects from the available formats. * @param representations The representations to be considered by the source. */ - public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator, + public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, Representation... representations) { - long periodDurationUs = (representations[0].periodDurationMs == -1) - ? -1 : representations[0].periodDurationMs * 1000; + this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator); + } + /** + * Lightweight constructor to use for fixed duration content. + * + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + * @param representations The representations to be considered by the source. + */ + public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, + List representations) { + this(buildManifest(representations), 0, null, dataSource, formatEvaluator); + } + + /** + * Constructor to use for fixed duration content. + * + * @param manifest The manifest. + * @param adaptationSetIndex The index of the adaptation set that should be used. + * @param representationIndices The indices of the representations within the adaptations set + * that should be used. May be null if all representations within the adaptation set should + * be considered. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + */ + public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, + int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { + this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0); + } + + /** + * Constructor to use for live streaming. + *

+ * May also be used for fixed duration content, in which case the call is equivalent to calling + * the other constructor, passing {@code manifestFetcher.getManifest()} is the first argument. + * + * @param manifestFetcher A fetcher for the manifest, which must have already successfully + * completed an initial load. + * @param adaptationSetIndex The index of the adaptation set that should be used. + * @param representationIndices The indices of the representations within the adaptations set + * that should be used. May be null if all representations within the adaptation set should + * be considered. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should + * lag behind the "live edge" (i.e. the end of the most recently defined media in the + * manifest). Choosing a small value will minimize latency introduced by the player, however + * note that the value sets an upper bound on the length of media that the player can buffer. + * Hence a small value may increase the probability of rebuffering and playback failures. + */ + public DashChunkSource(ManifestFetcher manifestFetcher, + int adaptationSetIndex, int[] representationIndices, DataSource dataSource, + FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { + this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, + dataSource, formatEvaluator, liveEdgeLatencyMs * 1000); + } + + private DashChunkSource(ManifestFetcher manifestFetcher, + MediaPresentationDescription initialManifest, int adaptationSetIndex, + int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, + long liveEdgeLatencyUs) { + this.manifestFetcher = manifestFetcher; + this.currentManifest = initialManifest; + this.adaptationSetIndex = adaptationSetIndex; + this.representationIndices = representationIndices; this.dataSource = dataSource; - this.evaluator = evaluator; - this.formats = new Format[representations.length]; - this.extractors = new HashMap(); - this.segmentIndexes = new HashMap(); - this.representations = new HashMap(); - this.trackInfo = new TrackInfo(representations[0].format.mimeType, periodDurationUs); + this.evaluator = formatEvaluator; + this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.evaluation = new Evaluation(); + this.headerBuilder = new StringBuilder(); + + Representation[] representations = getFilteredRepresentations(currentManifest, + adaptationSetIndex, representationIndices); + long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US) + ? TrackRenderer.UNKNOWN_TIME_US : representations[0].periodDurationMs * 1000; + this.trackInfo = new TrackInfo(representations[0].format.mimeType, periodDurationUs); + + this.formats = new Format[representations.length]; + this.representationHolders = new HashMap(); int maxWidth = 0; int maxHeight = 0; for (int i = 0; i < representations.length; i++) { formats[i] = representations[i].format; maxWidth = Math.max(formats[i].width, maxWidth); maxHeight = Math.max(formats[i].height, maxHeight); - Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) - ? new WebmExtractor() : new FragmentedMp4Extractor(); - extractors.put(formats[i].id, extractor); - this.representations.put(formats[i].id, representations[i]); - DashSegmentIndex segmentIndex = representations[i].getIndex(); - if (segmentIndex != null) { - segmentIndexes.put(formats[i].id, segmentIndex); - } + Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() + : new FragmentedMp4Extractor(); + representationHolders.put(formats[i].id, + new RepresentationHolder(representations[i], extractor)); } this.maxWidth = maxWidth; this.maxHeight = maxHeight; @@ -118,21 +219,67 @@ public class DashChunkSource implements ChunkSource { @Override public void enable() { evaluator.enable(); + if (manifestFetcher != null) { + manifestFetcher.enable(); + } } @Override public void disable(List queue) { evaluator.disable(); + if (manifestFetcher != null) { + manifestFetcher.disable(); + } } @Override public void continueBuffering(long playbackPositionUs) { - // Do nothing + if (manifestFetcher == null || !currentManifest.dynamic || fatalError != null) { + return; + } + + MediaPresentationDescription newManifest = manifestFetcher.getManifest(); + if (currentManifest != newManifest && newManifest != null) { + Representation[] newRepresentations = DashChunkSource.getFilteredRepresentations(newManifest, + adaptationSetIndex, representationIndices); + for (Representation representation : newRepresentations) { + RepresentationHolder representationHolder = + representationHolders.get(representation.format.id); + DashSegmentIndex oldIndex = representationHolder.segmentIndex; + DashSegmentIndex newIndex = representation.getIndex(); + int newFirstSegmentNum = newIndex.getFirstSegmentNum(); + int segmentNumShift = oldIndex.getSegmentNum(newIndex.getTimeUs(newFirstSegmentNum)) + - newFirstSegmentNum; + representationHolder.segmentNumShift += segmentNumShift; + representationHolder.segmentIndex = newIndex; + } + currentManifest = newManifest; + finishedCurrentManifest = false; + } + + // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where + // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit + // signaling in the stream, according to: + // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ + long minUpdatePeriod = currentManifest.minUpdatePeriod; + if (minUpdatePeriod == 0) { + minUpdatePeriod = 5000; + } + + if (finishedCurrentManifest && (SystemClock.elapsedRealtime() + > manifestFetcher.getManifestLoadTimestamp() + minUpdatePeriod)) { + manifestFetcher.requestRefresh(); + } } @Override public final void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, ChunkOperationHolder out) { + if (fatalError != null) { + out.chunk = null; + return; + } + evaluation.queueSize = queue.size(); if (evaluation.format == null || !lastChunkWasInitialization) { evaluator.evaluate(queue, playbackPositionUs, formats, evaluation); @@ -150,17 +297,21 @@ public class DashChunkSource implements ChunkSource { return; } - Representation selectedRepresentation = representations.get(selectedFormat.id); - Extractor extractor = extractors.get(selectedRepresentation.format.id); + RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id); + Representation selectedRepresentation = representationHolder.representation; + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + Extractor extractor = representationHolder.extractor; RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; + if (extractor.getFormat() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } - if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { + if (segmentIndex == null) { pendingIndexUri = selectedRepresentation.getIndexUri(); } + if (pendingInitializationUri != null || pendingIndexUri != null) { // We have initialization and/or index requests to make. Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, @@ -170,28 +321,48 @@ public class DashChunkSource implements ChunkSource { return; } - int nextSegmentNum; - DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); + int segmentNum; if (queue.isEmpty()) { - nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); + if (currentManifest.dynamic) { + seekPositionUs = getLiveSeekPosition(); + } + segmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { - nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; + segmentNum = queue.get(out.queueSize - 1).nextChunkIndex + - representationHolder.segmentNumShift; } - if (nextSegmentNum == -1) { + if (currentManifest.dynamic) { + if (segmentNum < segmentIndex.getFirstSegmentNum()) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } else if (segmentNum > segmentIndex.getLastSegmentNum()) { + // This is beyond the last chunk in the current manifest. + finishedCurrentManifest = true; + return; + } else if (segmentNum == segmentIndex.getLastSegmentNum()) { + // This is the last chunk in the current manifest. Mark the manifest as being finished, + // but continue to return the final chunk. + finishedCurrentManifest = true; + } + } + + if (segmentNum == -1) { out.chunk = null; return; } - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, - dataSource, nextSegmentNum, evaluation.trigger); + Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum, + evaluation.trigger); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @Override public IOException getError() { - return null; + return fatalError != null ? fatalError + : (manifestFetcher != null ? manifestFetcher.getError() : null); } @Override @@ -231,22 +402,90 @@ public class DashChunkSource implements ChunkSource { } DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, representation.getCacheKey()); + return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format, extractor, expectedExtractorResult, indexAnchor); } - private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, - Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { - int lastSegmentNum = segmentIndex.getLastSegmentNum(); - int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; + private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, + int segmentNum, int trigger) { + Representation representation = representationHolder.representation; + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + long startTimeUs = segmentIndex.getTimeUs(segmentNum); - long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) - : startTimeUs + segmentIndex.getDurationUs(segmentNum); + long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum); + + boolean isLastSegment = !currentManifest.dynamic + && segmentNum == segmentIndex.getLastSegmentNum(); + int nextAbsoluteSegmentNum = isLastSegment ? -1 + : (representationHolder.segmentNumShift + segmentNum + 1); + RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, representation.getCacheKey()); - return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, - endTimeUs, nextSegmentNum, extractor, false, 0); + + long presentationTimeOffsetUs = representation.presentationTimeOffsetMs * 1000; + if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) { + if (representationHolder.vttHeaderOffsetUs != presentationTimeOffsetUs) { + // Update the VTT header. + headerBuilder.setLength(0); + headerBuilder.append(WebvttParser.EXO_HEADER).append("=") + .append(WebvttParser.OFFSET).append(presentationTimeOffsetUs).append("\n"); + representationHolder.vttHeader = headerBuilder.toString().getBytes(); + representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs; + } + return new SingleSampleMediaChunk(dataSource, dataSpec, representation.format, 0, + startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); + } else { + return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, + endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, false, + presentationTimeOffsetUs); + } + } + + /** + * For live playbacks, determines the seek position that snaps playback to be + * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest + * + * @return The seek position in microseconds. + */ + private long getLiveSeekPosition() { + long liveEdgeTimestampUs = Long.MIN_VALUE; + for (RepresentationHolder representationHolder : representationHolders.values()) { + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) + + segmentIndex.getDurationUs(lastSegmentNum); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); + } + return liveEdgeTimestampUs - liveEdgeLatencyUs; + } + + private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, + int adaptationSetIndex, int[] representationIndices) { + List representations = + manifest.periods.get(0).adaptationSets.get(adaptationSetIndex).representations; + if (representationIndices == null) { + Representation[] filteredRepresentations = new Representation[representations.size()]; + representations.toArray(filteredRepresentations); + return filteredRepresentations; + } else { + Representation[] filteredRepresentations = new Representation[representationIndices.length]; + for (int i = 0; i < representationIndices.length; i++) { + filteredRepresentations[i] = representations.get(representationIndices[i]); + } + return filteredRepresentations; + } + } + + private static MediaPresentationDescription buildManifest(List representations) { + Representation firstRepresentation = representations.get(0); + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); + Period period = new Period(null, firstRepresentation.periodStartMs, + firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); + long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; + return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null, + Collections.singletonList(period)); } private class InitializationLoadable extends Chunk { @@ -274,11 +513,30 @@ public class DashChunkSource implements ChunkSource { + expectedExtractorResult + ", got " + result); } if ((result & Extractor.RESULT_READ_INDEX) != 0) { - segmentIndexes.put(format.id, - new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor)); + representationHolders.get(format.id).segmentIndex = + new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor); } } } + private static class RepresentationHolder { + + public final Representation representation; + public final Extractor extractor; + + public DashSegmentIndex segmentIndex; + public int segmentNumShift; + + public long vttHeaderOffsetUs; + public byte[] vttHeader; + + public RepresentationHolder(Representation representation, Extractor extractor) { + this.representation = representation; + this.extractor = extractor; + this.segmentIndex = representation.getIndex(); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java index 3e64cb9853..c1cc738661 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java @@ -56,4 +56,21 @@ public class Period { this.adaptationSets = Collections.unmodifiableList(adaptationSets); } + /** + * Returns the index of the first adaptation set of a given type, or -1 if no adaptation set of + * the specified type exists. + * + * @param type An adaptation set type. + * @return The index of the first adaptation set of the specified type, or -1. + */ + public int getAdaptationSetIndex(int type) { + int adaptationCount = adaptationSets.size(); + for (int i = 0; i < adaptationCount; i++) { + if (adaptationSets.get(i).type == type) { + return i; + } + } + return -1; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 9cf8274207..9aed794b22 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.util; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; +import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.Pair; @@ -29,12 +30,25 @@ import java.net.URLConnection; import java.util.concurrent.CancellationException; /** - * Performs both single and repeated loads of media manfifests. + * Performs both single and repeated loads of media manifests. * * @param The type of manifest. */ public class ManifestFetcher implements Loader.Callback { + /** + * Interface definition for a callback to be notified of {@link ManifestFetcher} events. + */ + public interface EventListener { + + public void onManifestRefreshStarted(); + + public void onManifestRefreshed(); + + public void onManifestError(IOException e); + + } + /** * Callback for the result of a single load. * @@ -61,9 +75,12 @@ public class ManifestFetcher implements Loader.Callback { } /* package */ final ManifestParser parser; - /* package */ final String manifestUrl; /* package */ final String contentId; /* package */ final String userAgent; + private final Handler eventHandler; + private final EventListener eventListener; + + /* package */ volatile String manifestUrl; private int enabledCount; private Loader loader; @@ -76,6 +93,11 @@ public class ManifestFetcher implements Loader.Callback { private volatile T manifest; private volatile long manifestLoadTimestamp; + public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl, + String userAgent) { + this(parser, contentId, manifestUrl, userAgent, null, null); + } + /** * @param parser A parser to parse the loaded manifest data. * @param contentId The content id of the content being loaded. May be null. @@ -83,11 +105,22 @@ public class ManifestFetcher implements Loader.Callback { * @param userAgent The User-Agent string that should be used. */ public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl, - String userAgent) { + String userAgent, Handler eventHandler, EventListener eventListener) { this.parser = parser; this.contentId = contentId; this.manifestUrl = manifestUrl; this.userAgent = userAgent; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + } + + /** + * Updates the manifest location. + * + * @param manifestUrl The manifest location. + */ + public void updateManifestUrl(String manifestUrl) { + this.manifestUrl = manifestUrl; } /** @@ -173,6 +206,7 @@ public class ManifestFetcher implements Loader.Callback { if (!loader.isLoading()) { currentLoadable = new ManifestLoadable(); loader.startLoading(currentLoadable, this); + notifyManifestRefreshStarted(); } } @@ -187,6 +221,8 @@ public class ManifestFetcher implements Loader.Callback { manifestLoadTimestamp = SystemClock.elapsedRealtime(); loadExceptionCount = 0; loadException = null; + + notifyManifestRefreshed(); } @Override @@ -204,12 +240,47 @@ public class ManifestFetcher implements Loader.Callback { loadExceptionCount++; loadExceptionTimestamp = SystemClock.elapsedRealtime(); loadException = new IOException(exception); + + notifyManifestError(loadException); } private long getRetryDelayMillis(long errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } + private void notifyManifestRefreshStarted() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestRefreshStarted(); + } + }); + } + } + + private void notifyManifestRefreshed() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestRefreshed(); + } + }); + } + } + + private void notifyManifestError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestError(e); + } + }); + } + } + private class SingleFetchHelper implements Loader.Callback { private final Looper callbackLooper; diff --git a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java index 17027a8d91..b9c4899de2 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java +++ b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java @@ -70,12 +70,14 @@ public class PlayerControl implements MediaPlayerControl { @Override public int getCurrentPosition() { - return (int) exoPlayer.getCurrentPosition(); + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getCurrentPosition(); } @Override public int getDuration() { - return (int) exoPlayer.getDuration(); + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getDuration(); } @Override @@ -95,8 +97,9 @@ public class PlayerControl implements MediaPlayerControl { @Override public void seekTo(int timeMillis) { - // MediaController arrow keys generate unbounded values. - exoPlayer.seekTo(Math.min(Math.max(0, timeMillis), getDuration())); + long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : Math.min(Math.max(0, timeMillis), getDuration()); + exoPlayer.seekTo(seekPosition); } } diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index b8cd40215d..95f9576d41 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -399,4 +399,22 @@ public final class Util { return scaledTimestamps; } + /** + * Converts a list of integers to a primitive array. + * + * @param list A list of integers. + * @return The list in array form, or null if the input list was null. + */ + public static int[] toArray(List list) { + if (list == null) { + return null; + } + int length = list.size(); + int[] intArray = new int[length]; + for (int i = 0; i < length; i++) { + intArray[i] = list.get(i); + } + return intArray; + } + } From fc8c08d240ee0bac3d40e8d816c780bdf0ab0178 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 5 Dec 2014 17:51:52 +0000 Subject: [PATCH 102/110] Fix #187 --- .../com/google/android/exoplayer/upstream/HttpDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f9d3bf8f1a..164820654c 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 @@ -376,7 +376,7 @@ public class HttpDataSource implements DataSource { connection.setReadTimeout(readTimeoutMillis); connection.setDoOutput(false); synchronized (requestProperties) { - for (HashMap.Entry property : requestProperties.entrySet()) { + for (Map.Entry property : requestProperties.entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } } From 6f1832fb66df5f9b8844d595ab0b13ef7dab3e34 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 5 Dec 2014 17:52:30 +0000 Subject: [PATCH 103/110] Support negative-fronted xs:duration values. Issue: 186 --- .../google/android/exoplayer/util/Util.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 95f9576d41..4d51fda9b0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -55,7 +55,7 @@ public final class Util { + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = - Pattern.compile("^P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private Util() {} @@ -275,21 +275,23 @@ public final class Util { public static long parseXsDuration(String value) { Matcher matcher = XS_DURATION_PATTERN.matcher(value); if (matcher.matches()) { + boolean negated = !TextUtils.isEmpty(matcher.group(1)); // Durations containing years and months aren't completely defined. We assume there are // 30.4368 days in a month, and 365.242 days in a year. - String years = matcher.group(2); + String years = matcher.group(3); double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0; - String months = matcher.group(4); + String months = matcher.group(5); durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0; - String days = matcher.group(6); + String days = matcher.group(7); durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0; - String hours = matcher.group(9); + String hours = matcher.group(10); durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0; - String minutes = matcher.group(11); + String minutes = matcher.group(12); durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0; - String seconds = matcher.group(13); + String seconds = matcher.group(14); durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0; - return (long) (durationSeconds * 1000); + long durationMillis = (long) (durationSeconds * 1000); + return negated ? -durationMillis : durationMillis; } else { return (long) (Double.parseDouble(value) * 3600 * 1000); } From c8e5988e6d2ca8c2391042adb2f3ea0443f937f3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 5 Dec 2014 17:53:24 +0000 Subject: [PATCH 104/110] Fix handling of unknown duration in FMP4. Issue: 186 --- .../parser/mp4/FragmentedMp4Extractor.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) 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 c9d2caf953..9950aecd2a 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 @@ -462,7 +462,8 @@ public final class FragmentedMp4Extractor implements Extractor { /** * Parses a tkhd atom (defined in 14496-12). * - * @return A {@link Pair} consisting of the track id and duration. + * @return A {@link Pair} consisting of the track id and duration (in the timescale indicated in + * the movie header box). The duration is set to -1 if the duration is unspecified. */ private static Pair parseTkhd(ParsableByteArray tkhd) { tkhd.setPosition(ATOM_HEADER_SIZE); @@ -473,7 +474,23 @@ public final class FragmentedMp4Extractor implements Extractor { int trackId = tkhd.readInt(); tkhd.skip(4); - long duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); + + boolean durationUnknown = true; + int durationPosition = tkhd.getPosition(); + int durationByteCount = version == 0 ? 4 : 8; + for (int i = 0; i < durationByteCount; i++) { + if (tkhd.data[durationPosition + i] != -1) { + durationUnknown = false; + break; + } + } + long duration; + if (durationUnknown) { + tkhd.skip(durationByteCount); + duration = -1; + } else { + duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); + } return Pair.create(trackId, duration); } From 01151c9c65810a9fff439f5901a85bde4e6a4f63 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:10:52 +0000 Subject: [PATCH 105/110] Don't append base uri if chunkUrl is absolute. --- .../exoplayer/smoothstreaming/SmoothStreamingManifest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 271949b58b..7a6a32e44a 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -274,7 +274,7 @@ public class SmoothStreamingManifest { String chunkUrl = chunkTemplate .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) .replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes.get(chunkIndex))); - return baseUri.buildUpon().appendEncodedPath(chunkUrl).build(); + return Util.getMergedUri(baseUri, chunkUrl); } } From cf80c4d9cb3de06e9861bb85ca3242ded6e6b072 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:12:04 +0000 Subject: [PATCH 106/110] Allow passing of optional parameters in MediaDrm key requests. --- .../demo/full/player/DashRendererBuilder.java | 4 ++-- .../SmoothStreamingRendererBuilder.java | 2 +- .../drm/StreamingDrmSessionManager.java | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index f998d6e30f..8ffd60218a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java @@ -279,8 +279,8 @@ public class DashRendererBuilder implements RendererBuilder, public static Pair getDrmSessionManagerData(DemoPlayer player, MediaDrmCallback drmCallback) throws UnsupportedSchemeException { StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager( - DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, player.getMainHandler(), - player); + DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, null, + player.getMainHandler(), player); return Pair.create((DrmSessionManager) streamingDrmSessionManager, getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 7d88519b45..fd9c220cb2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -252,7 +252,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player, MediaDrmCallback drmCallback) throws UnsupportedSchemeException { - return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, + return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); } diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index 8d4e697d4d..866c5f96ef 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -30,6 +30,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -61,6 +62,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private final Handler eventHandler; private final EventListener eventListener; private final MediaDrm mediaDrm; + private final HashMap optionalKeyRequestParameters; /* package */ final MediaDrmHandler mediaDrmHandler; /* package */ final MediaDrmCallback callback; @@ -79,20 +81,33 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private byte[] schemePsshData; private byte[] sessionId; + /** + * @deprecated Use the other constructor, passing null as {@code optionalKeyRequestParameters}. + */ + @Deprecated + public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback, + Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException { + this(uuid, playbackLooper, callback, null, eventHandler, eventListener); + } + /** * @param uuid The UUID of the drm scheme. * @param playbackLooper The looper associated with the media playback thread. Should usually be * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}. * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @throws UnsupportedSchemeException If the specified DRM scheme is not supported. */ public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback, - Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException { + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener) throws UnsupportedSchemeException { this.uuid = uuid; this.callback = callback; + this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; mediaDrm = new MediaDrm(uuid); @@ -250,7 +265,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { KeyRequest keyRequest; try { keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType, - MediaDrm.KEY_TYPE_STREAMING, null); + MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); } catch (NotProvisionedException e) { onKeysError(e); From 2f0a1779e2183b990885b099d1beed38438d9bac Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:13:52 +0000 Subject: [PATCH 107/110] Stop piping PSSH information through the extractor. It's cleaner to not inject data into the extractor only so that it can be read out as though it were parsed from the stream. This is also an incremental step towards fixing Github issue #119. --- .../exoplayer/chunk/Mp4MediaChunk.java | 23 ++++++++++++++++--- .../exoplayer/dash/DashChunkSource.java | 2 +- .../parser/mp4/FragmentedMp4Extractor.java | 14 ----------- .../SmoothStreamingChunkSource.java | 17 ++++++++------ 4 files changed, 31 insertions(+), 25 deletions(-) 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 a4d05cacd7..e39c53ebff 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 @@ -40,6 +40,17 @@ public final class Mp4MediaChunk extends MediaChunk { private MediaFormat mediaFormat; private Map psshInfo; + /** + * @deprecated Use the other constructor, passing null as {@code psshInfo}. + */ + @Deprecated + public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, + int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, + Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { + this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex, + extractor, null, maybeSelfContained, sampleOffsetUs); + } + /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. @@ -49,6 +60,8 @@ public final class Mp4MediaChunk extends MediaChunk { * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param extractor The extractor that will be used to extract the samples. + * @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it + * can be obtained directly from {@code extractor}, or if no pssh data is required. * @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might * contain a moov atom defining the media format of the chunk. This parameter can always be * safely set to true. Setting to false where the chunk is known to not be self contained may @@ -56,12 +69,13 @@ public final class Mp4MediaChunk extends MediaChunk { * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor. */ public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, - int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, - Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { + int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor, + Map psshInfo, boolean maybeSelfContained, long sampleOffsetUs) { super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); this.extractor = extractor; this.maybeSelfContained = maybeSelfContained; this.sampleOffsetUs = sampleOffsetUs; + this.psshInfo = psshInfo; } @Override @@ -97,7 +111,10 @@ public final class Mp4MediaChunk extends MediaChunk { } if (prepared) { mediaFormat = extractor.getFormat(); - psshInfo = extractor.getPsshInfo(); + Map extractorPsshInfo = extractor.getPsshInfo(); + if (extractorPsshInfo != null) { + psshInfo = extractorPsshInfo; + } } } return prepared; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 9bcb1aa3b8..d0c123bdc9 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -438,7 +438,7 @@ public class DashChunkSource implements ChunkSource { startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); } else { return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, - endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, false, + endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, null, false, presentationTimeOffsetUs); } } 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 9950aecd2a..34f0404083 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 @@ -189,20 +189,6 @@ public final class FragmentedMp4Extractor implements Extractor { this.track = track; } - /** - * Sideloads pssh information into the extractor, so that it can be read through - * {@link #getPsshInfo()}. - * - * @param uuid The UUID of the scheme for which information is being sideloaded. - * @param data The corresponding data. - */ - public void putPsshInfo(UUID uuid, byte[] data) { - // TODO: This is for SmoothStreaming. Consider using something other than - // FragmentedMp4Extractor.getPsshInfo to obtain the pssh data for that use case, so that we can - // remove this method. - psshData.put(uuid, data); - } - @Override public Map getPsshInfo() { return psshData.isEmpty() ? null : psshData; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 2b676e6b52..936fdf824d 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -48,6 +48,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; /** * An {@link ChunkSource} for SmoothStreaming. @@ -69,6 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { private final int maxHeight; private final SparseArray extractors; + private final Map psshInfo; private final SmoothStreamingFormat[] formats; private SmoothStreamingManifest currentManifest; @@ -140,6 +143,9 @@ public class SmoothStreamingChunkSource implements ChunkSource { byte[] keyId = getKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); + psshInfo = Collections.singletonMap(protectionElement.uuid, protectionElement.data); + } else { + psshInfo = null; } int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; @@ -163,9 +169,6 @@ public class SmoothStreamingChunkSource implements ChunkSource { FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, mediaFormat, trackEncryptionBoxes)); - if (protectionElement != null) { - extractor.putPsshInfo(protectionElement.uuid, protectionElement.data); - } extractors.put(trackIndex, extractor); } this.maxHeight = maxHeight; @@ -296,8 +299,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, - extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, currentAbsoluteChunkIndex, - isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); + extractors.get(Integer.parseInt(selectedFormat.id)), psshInfo, dataSource, + currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); out.chunk = mediaChunk; } @@ -361,7 +364,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { } private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, - Extractor extractor, DataSource dataSource, int chunkIndex, + Extractor extractor, Map psshInfo, DataSource dataSource, int chunkIndex, boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) { int nextChunkIndex = isLast ? -1 : chunkIndex + 1; long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs; @@ -370,7 +373,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs, - nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs); + nextStartTimeUs, nextChunkIndex, extractor, psshInfo, false, -chunkStartTimeUs); } private static byte[] getKeyId(byte[] initData) { From c4b2a0121230cb533a658fd6c55b4c01199a3000 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:15:06 +0000 Subject: [PATCH 108/110] Allow out-of-band pssh data for DASH playbacks. This fixes the referenced issue, except that the MPD parser needs to actually parse out UUID and binary data for schemes that we wish to support. Alternatively, it's easy to applications to do this themselves by extending the parser and overriding the parseContentProtection and buildContentProtection methods. Github Issue: #119 --- .../exoplayer/dash/DashChunkSource.java | 27 ++++++++++++++++--- .../exoplayer/dash/mpd/ContentProtection.java | 20 ++++++++++++-- .../MediaPresentationDescriptionParser.java | 2 +- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index d0c123bdc9..932a8ea598 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.Mp4MediaChunk; import com.google.android.exoplayer.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.ContentProtection; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.RangedUri; @@ -53,6 +54,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; /** * An {@link ChunkSource} for DASH streams. @@ -92,6 +95,7 @@ public class DashChunkSource implements ChunkSource { private final ManifestFetcher manifestFetcher; private final int adaptationSetIndex; private final int[] representationIndices; + private final Map psshInfo; private MediaPresentationDescription currentManifest; private boolean finishedCurrentManifest; @@ -180,6 +184,7 @@ public class DashChunkSource implements ChunkSource { this.evaluation = new Evaluation(); this.headerBuilder = new StringBuilder(); + psshInfo = getPsshInfo(currentManifest, adaptationSetIndex); Representation[] representations = getFilteredRepresentations(currentManifest, adaptationSetIndex, representationIndices); long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US) @@ -438,7 +443,7 @@ public class DashChunkSource implements ChunkSource { startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); } else { return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, - endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, null, false, + endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo, false, presentationTimeOffsetUs); } } @@ -463,8 +468,8 @@ public class DashChunkSource implements ChunkSource { private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, int adaptationSetIndex, int[] representationIndices) { - List representations = - manifest.periods.get(0).adaptationSets.get(adaptationSetIndex).representations; + AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); + List representations = adaptationSet.representations; if (representationIndices == null) { Representation[] filteredRepresentations = new Representation[representations.size()]; representations.toArray(filteredRepresentations); @@ -478,6 +483,22 @@ public class DashChunkSource implements ChunkSource { } } + private static Map getPsshInfo(MediaPresentationDescription manifest, + int adaptationSetIndex) { + AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); + if (adaptationSet.contentProtections.isEmpty()) { + return null; + } else { + Map psshInfo = new HashMap(); + for (ContentProtection contentProtection : adaptationSet.contentProtections) { + if (contentProtection.uuid != null && contentProtection.data != null) { + psshInfo.put(contentProtection.uuid, contentProtection.data); + } + } + return psshInfo.isEmpty() ? null : psshInfo; + } + } + private static MediaPresentationDescription buildManifest(List representations) { Representation firstRepresentation = representations.get(0); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java index bd6acca9af..c8f7cfb501 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.dash.mpd; +import java.util.UUID; + /** * Represents a ContentProtection tag in an AdaptationSet. */ @@ -26,10 +28,24 @@ public class ContentProtection { public final String schemeUriId; /** - * @param schemeUriId Identifies the content protection scheme. + * The UUID of the protection scheme. May be null. */ - public ContentProtection(String schemeUriId) { + public final UUID uuid; + + /** + * Protection scheme specific data. May be null. + */ + public final byte[] data; + + /** + * @param schemeUriId Identifies the content protection scheme. + * @param uuid The UUID of the protection scheme, if known. May be null. + * @param data Protection scheme specific initialization data. May be null. + */ + public ContentProtection(String schemeUriId, UUID uuid, byte[] data) { this.schemeUriId = schemeUriId; + this.uuid = uuid; + this.data = data; } } 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 bf1ba532b7..a8ed7c03f2 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 @@ -257,7 +257,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } protected ContentProtection buildContentProtection(String schemeIdUri) { - return new ContentProtection(schemeIdUri); + return new ContentProtection(schemeIdUri, null, null); } /** From 86b2209ad03ebccdb4212b8f1bd470cc595bb10f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 10 Dec 2014 14:04:58 +0000 Subject: [PATCH 109/110] Bump version to 1.1.0. Also update gradle files. --- build.gradle | 2 +- demo/build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- library/build.gradle | 2 +- .../com/google/android/exoplayer/ExoPlayerLibraryInfo.java | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 68d066ea65..fb8f3092c8 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.14.+' + classpath 'com.android.tools.build:gradle:1.0.0-rc1' } } diff --git a/demo/build.gradle b/demo/build.gradle index 91a06b8b07..0a530c8609 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 21 - buildToolsVersion "19.1" + buildToolsVersion "21.1.1" defaultConfig { minSdkVersion 16 diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index f590303052..ee2f978324 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/library/build.gradle b/library/build.gradle index 8ccf3cfaca..8256b871f5 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'com.android.library' android { compileSdkVersion 21 - buildToolsVersion "19.1" + buildToolsVersion "21.1.1" defaultConfig { minSdkVersion 9 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 cc3265d18a..6fb7d20be8 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,15 +26,15 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.0.13"; + public static final String VERSION = "1.1.0"; /** * The version of the library, expressed as an integer. *

* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the - * corresponding integer version 1002003. + * corresponding integer version 001002003. */ - public static final int VERSION_INT = 1000013; + public static final int VERSION_INT = 001001000; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} From f15e3973e05cfbb88c1283a5b8869c7e18c093cf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 10 Dec 2014 14:05:51 +0000 Subject: [PATCH 110/110] Fix discovery of secure decoders on some L devices. --- .../android/exoplayer/MediaCodecUtil.java | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 3dfe5f1afe..62918b531f 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -24,6 +24,7 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; import android.text.TextUtils; +import android.util.Log; import android.util.Pair; import java.util.HashMap; @@ -34,6 +35,8 @@ import java.util.HashMap; @TargetApi(16) public class MediaCodecUtil { + private static final String TAG = "MediaCodecUtil"; + private static final HashMap> codecs = new HashMap>(); @@ -77,6 +80,23 @@ public class MediaCodecUtil { } MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); + Pair codecInfo = getMediaCodecInfo(key, mediaCodecList); + // TODO: Verify this cannot occur on v22, and change >= to == [Internal: b/18678462]. + if (secure && codecInfo == null && Util.SDK_INT >= 21) { + // Some devices don't list secure decoders on API level 21. Try the legacy path. + mediaCodecList = new MediaCodecListCompatV16(); + codecInfo = getMediaCodecInfo(key, mediaCodecList); + if (codecInfo != null) { + Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + + ". Assuming: " + codecInfo.first); + } + } + return codecInfo; + } + + private static Pair getMediaCodecInfo(CodecKey key, + MediaCodecListCompat mediaCodecList) { + String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); // Note: MediaCodecList is sorted by the framework such that the best decoders come first. @@ -90,18 +110,19 @@ public class MediaCodecUtil { String supportedType = supportedTypes[j]; if (supportedType.equalsIgnoreCase(mimeType)) { CodecCapabilities capabilities = info.getCapabilitiesForType(supportedType); + boolean secure = mediaCodecList.isSecurePlaybackSupported(key.mimeType, capabilities); if (!secureDecodersExplicit) { - // Cache variants for secure and insecure playback. Note that the secure decoder is - // inferred, and may not actually exist. + // Cache variants for both insecure and (if we think it's supported) secure playback. codecs.put(key.secure ? new CodecKey(mimeType, false) : key, Pair.create(codecName, capabilities)); - codecs.put(key.secure ? key : new CodecKey(mimeType, true), - Pair.create(codecName + ".secure", capabilities)); + if (secure) { + codecs.put(key.secure ? key : new CodecKey(mimeType, true), + Pair.create(codecName + ".secure", capabilities)); + } } else { - // We can only cache this variant. The other should be listed explicitly. - boolean codecSecure = mediaCodecList.isSecurePlaybackSupported( - info.getCapabilitiesForType(supportedType)); - codecs.put(key.secure == codecSecure ? key : new CodecKey(mimeType, codecSecure), + // Only cache this variant. If both insecure and secure decoders are available, they + // should both be listed separately. + codecs.put(key.secure == secure ? key : new CodecKey(mimeType, secure), Pair.create(codecName, capabilities)); } if (codecs.containsKey(key)) { @@ -219,10 +240,8 @@ public class MediaCodecUtil { /** * Whether secure playback is supported for the given {@link CodecCapabilities}, which should * have been obtained from a {@link MediaCodecInfo} obtained from this list. - *

- * May only be called if {@link #secureDecodersExplicit()} returns true. */ - public boolean isSecurePlaybackSupported(CodecCapabilities capabilities); + public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities); } @@ -252,7 +271,7 @@ public class MediaCodecUtil { } @Override - public boolean isSecurePlaybackSupported(CodecCapabilities capabilities) { + public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); } @@ -277,8 +296,10 @@ public class MediaCodecUtil { } @Override - public boolean isSecurePlaybackSupported(CodecCapabilities capabilities) { - throw new UnsupportedOperationException(); + public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) { + // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure + // H264 decoder exists. + return MimeTypes.VIDEO_H264.equals(mimeType); } }