diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 234c91daba..f45cb9aff6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,52 @@ # Release notes # +### r2.3.0 ### + +* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio + rendering. You can read more about the GVR extension + [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). +* DASH improvements: + * Support embedded CEA-608 closed captions + ([#2362](https://github.com/google/ExoPlayer/issues/2362)). + * Support embedded EMSG events + ([#2176](https://github.com/google/ExoPlayer/issues/2176)). + * Support mspr:pro manifest element + ([#2386](https://github.com/google/ExoPlayer/issues/2386)). + * Correct handling of empty segment indices at the start of live events + ([#1865](https://github.com/google/ExoPlayer/issues/1865)). +* HLS improvements: + * Respect initial track selection + ([#2353](https://github.com/google/ExoPlayer/issues/2353)). + * Reduced frequency of media playlist requests when playback position is close + to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)). + * Exposed the master playlist through ExoPlayer.getCurrentManifest() + ([#2537](https://github.com/google/ExoPlayer/issues/2537)). + * Support CLOSED-CAPTIONS #EXT-X-MEDIA type + ([#341](https://github.com/google/ExoPlayer/issues/341)). + * Fixed handling of negative values in #EXT-X-SUPPORT + ([#2495](https://github.com/google/ExoPlayer/issues/2495)). + * Fixed potential endless buffering state for streams with WebVTT subtitles + ([#2424](https://github.com/google/ExoPlayer/issues/2424)). +* MPEG-TS improvements: + * Support for multiple programs. + * Support for multiple closed captions and caption service descriptors + ([#2161](https://github.com/google/ExoPlayer/issues/2161)). +* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable + constant bitrate seeking in MP3 files that would otherwise be unseekable + ([#2445](https://github.com/google/ExoPlayer/issues/2445)). +* ID3: Better handle malformed ID3 data + ([#2486](https://github.com/google/ExoPlayer/issues/2486)). +* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector. +* DRM: Add support for CENC ClearKey on API level 21+ + ([#2361](https://github.com/google/ExoPlayer/issues/2361)). +* DRM: Support dynamic setting of key request headers + ([#1924](https://github.com/google/ExoPlayer/issues/1924)). +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* FLAC extension: Fix proguard configuration + ([#2427](https://github.com/google/ExoPlayer/issues/2427)). +* Misc bugfixes. + ### r2.2.0 ### * Demo app: Automatic recovery from BehindLiveWindowException, plus improved @@ -246,6 +293,12 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, however it can be assumed that all such changes are included in the most recent V2 release. +### r1.5.15 ### + +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* Misc bugfixes. + ### r1.5.14 ### * Fixed cache failures when using an encrypted cache content index. diff --git a/build.gradle b/build.gradle index b10a17de81..f1901a1270 100644 --- a/build.gradle +++ b/build.gradle @@ -11,16 +11,13 @@ // 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. - -// Top-level build file where you can add configuration options common to all sub-projects/modules. - buildscript { repositories { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' - classpath 'com.novoda:bintray-release:0.3.4' + classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.novoda:bintray-release:0.4.0' } } @@ -29,13 +26,24 @@ allprojects { jcenter() } project.ext { - compileSdkVersion=24 - targetSdkVersion=24 - buildToolsVersion='23.0.3' - releaseRepoName = 'exoplayer' + // Important: ExoPlayer specifies a minSdkVersion of 9 because various + // components provided by the library may be of use on older devices. + // However, please note that the core media playback functionality + // provided by the library requires API level 16 or greater. + minSdkVersion=9 + compileSdkVersion=25 + targetSdkVersion=25 + buildToolsVersion='25' + releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.2.0' + releaseVersion = 'r2.3.0' releaseWebsite = 'https://github.com/google/ExoPlayer' } } + +def getBintrayRepo() { + boolean publicRepo = hasProperty('publicRepo') && + property('publicRepo').toBoolean() + return publicRepo ? 'exoplayer' : 'exoplayer-test' +} diff --git a/demo/build.gradle b/demo/build.gradle index 007dc70590..01946c8504 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -33,6 +33,11 @@ android { } } + lintOptions { + // The demo app does not have translations. + disable 'MissingTranslation' + } + productFlavors { noExtensions withExtensions diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 2f3dc0d1bf..a834c5df19 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2300" + android:versionName="2.3.0"> diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 6fba5bd65b..dd88f206c1 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -277,6 +277,18 @@ } ] }, + { + "name": "ClearKey DASH", + "samples": [ + { + "name": "Big Buck Bunny (CENC ClearKey)", + "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", + "extension": "mpd", + "drm_scheme": "cenc", + "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" + } + ] + }, { "name": "SmoothStreaming", "samples": [ diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index edc268ddb9..e39cd16743 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -101,9 +101,6 @@ import java.util.Locale; @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - if (timeline == null) { - return; - } int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index bbfadf34af..adb04eaa24 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private DefaultTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; - private boolean playerNeedsSource; + private boolean needRetrySource; private boolean shouldAutoPlay; private int resumeWindow; @@ -231,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void initializePlayer() { Intent intent = getIntent(); - if (player == null) { + boolean needNewPlayer = player == null; + if (needNewPlayer) { boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; @@ -239,19 +238,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - Map keyRequestProperties; - if (keyRequestPropertiesArray == null || keyRequestPropertiesArray.length < 2) { - keyRequestProperties = null; - } else { - keyRequestProperties = new HashMap<>(); - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - keyRequestProperties.put(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } - } try { drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestProperties); + keyRequestPropertiesArray); } catch (UnsupportedDrmException e) { int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME @@ -267,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; TrackSelection.Factory videoTrackSelectionFactory = - new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); + new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), @@ -284,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay player.setPlayWhenReady(shouldAutoPlay); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - playerNeedsSource = true; } - if (playerNeedsSource) { + if (needNewPlayer || needRetrySource) { String action = intent.getAction(); Uri[] uris; String[] extensions; @@ -322,14 +310,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay player.seekTo(resumeWindow, resumePosition); } player.prepare(mediaSource, !haveResumePosition, false); - playerNeedsSource = false; + needRetrySource = false; updateButtonVisibilities(); } } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { - int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension - : uri.getLastPathSegment()); + int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) + : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: return new SsMediaSource(uri, buildDataSourceFactory(false), @@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } private DrmSessionManager buildDrmSessionManager(UUID uuid, - String licenseUrl, Map keyRequestProperties) throws UnsupportedDrmException { + String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false), keyRequestProperties); + buildHttpDataSourceFactory(false)); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], + keyRequestPropertiesArray[i + 1]); + } + } return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); } @@ -425,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public void onPositionDiscontinuity() { - if (playerNeedsSource) { + if (needRetrySource) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to // which they seeked. @@ -466,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (errorString != null) { showToast(errorString); } - playerNeedsSource = true; + needRetrySource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); initializePlayer(); @@ -498,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void updateButtonVisibilities() { debugRootView.removeAllViews(); - retryButton.setVisibility(playerNeedsSource ? View.VISIBLE : View.GONE); + retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE); debugRootView.addView(retryButton); if (player == null) { diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 946181284f..081ad190b5 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -262,11 +262,13 @@ public class SampleChooserActivity extends Activity { } private UUID getDrmUuid(String typeString) throws ParserException { - switch (typeString.toLowerCase()) { + switch (Util.toLowerInvariant(typeString)) { case "widevine": return C.WIDEVINE_UUID; case "playready": return C.PLAYREADY_UUID; + case "cenc": + return C.CLEARKEY_UUID; default: try { return UUID.fromString(typeString); diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index 936cdf90f8..576eb23c9d 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -51,7 +51,7 @@ import java.util.Locale; private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory(); private final MappingTrackSelector selector; - private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; + private final TrackSelection.Factory adaptiveTrackSelectionFactory; private MappedTrackInfo trackInfo; private int rendererIndex; @@ -67,13 +67,13 @@ import java.util.Locale; /** * @param selector The track selector. - * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, - * or null if the selection helper should not support adaptive video. + * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null + * if the selection helper should not support adaptive tracks. */ public TrackSelectionHelper(MappingTrackSelector selector, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { + TrackSelection.Factory adaptiveTrackSelectionFactory) { this.selector = selector; - this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; + this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; } /** @@ -92,7 +92,7 @@ import java.util.Locale; trackGroups = trackInfo.getTrackGroups(rendererIndex); trackGroupsAdaptive = new boolean[trackGroups.length]; for (int i = 0; i < trackGroups.length; i++) { - trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null + trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null && trackInfo.getAdaptiveSupport(rendererIndex, i, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED && trackGroups.get(i).length > 1; @@ -271,7 +271,7 @@ import java.util.Locale; private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) { TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY - : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveVideoTrackSelectionFactory); + : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory); override = new SelectionOverride(factory, group, tracks); } @@ -301,15 +301,18 @@ import java.util.Locale; private static String buildTrackName(Format format) { String trackName; if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), - buildBitrateString(format)), buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildAudioPropertyString(format)), buildBitrateString(format)), - buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildLanguageString(format), buildAudioPropertyString(format)), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } else { - trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } return trackName.length() == 0 ? "unknown" : trackName; } @@ -342,4 +345,8 @@ import java.util.Locale; return format.id == null ? "" : ("id:" + format.id); } + private static String buildSampleMimeTypeString(Format format) { + return format.sampleMimeType == null ? "" : format.sampleMimeType; + } + } diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index a245133937..f031a9dc48 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 31def44d36..246e23e172 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -118,7 +118,8 @@ public final class CronetDataSourceTest { TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, true, // resetTimeoutOnRedirects - mockClock)); + mockClock, + null)); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index f6202c6e1e..4f15a6eabc 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; - private final Map requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private final ConditionVariable operation; private final Clock clock; @@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou public CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener) { this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS, false); + DEFAULT_READ_TIMEOUT_MILLIS, false, null); } /** @@ -149,17 +149,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param defaultRequestProperties The default request properties to be used. */ public CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) { + int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, + RequestProperties defaultRequestProperties) { this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects, new SystemClock()); + readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties); } /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock) { + int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, + RequestProperties defaultRequestProperties) { this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); this.contentTypePredicate = contentTypePredicate; @@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.clock = Assertions.checkNotNull(clock); - requestProperties = new HashMap<>(); + this.defaultRequestProperties = defaultRequestProperties; + requestProperties = new RequestProperties(); operation = new ConditionVariable(); } @@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou @Override public void setRequestProperty(String name, String value) { - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(), this, executor); // Set the headers. - synchronized (requestProperties) { - if (dataSpec.postBody != null && dataSpec.postBody.length != 0 - && !requestProperties.containsKey(CONTENT_TYPE)) { - throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec, - Status.IDLE); - } - for (Entry headerEntry : requestProperties.entrySet()) { - requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue()); + boolean isContentTypeHeaderSet = false; + if (defaultRequestProperties != null) { + for (Entry headerEntry : defaultRequestProperties.getSnapshot().entrySet()) { + String key = headerEntry.getKey(); + isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key); + requestBuilder.addHeader(key, headerEntry.getValue()); } } + Map requestPropertiesSnapshot = requestProperties.getSnapshot(); + for (Entry headerEntry : requestPropertiesSnapshot.entrySet()) { + String key = headerEntry.getKey(); + isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key); + requestBuilder.addHeader(key, headerEntry.getValue()); + } + if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) { + throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec, + Status.IDLE); + } // Set the Range header. if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) { StringBuilder rangeValue = new StringBuilder(); diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 3df901ce59..db560305a7 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.cronet; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.TransferListener; @@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory { } @Override - protected CronetDataSource createDataSourceInternal() { + protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties + defaultRequestProperties) { return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, - connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects); + connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, null, defaultRequestProperties); } } diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 0d669f826d..beafcb6a96 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -31,9 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" NDK_PATH="" ``` -* Fetch and build FFmpeg. - -For example, to fetch and build for armv7a: +* Fetch and build FFmpeg. For example, to fetch and build for armv7a: ``` cd "${FFMPEG_EXT_PATH}/jni" && \ @@ -69,15 +67,14 @@ make -j4 && \ make install-libs ``` -* Build the JNI native libraries. +* Build the JNI native libraries. Repeat this step for any other architectures + you need to support. ``` cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 ``` -Repeat these steps for any other architectures you need to support. - * In your project, you can add a dependency on the extension by using a rule like this: diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index e0f6d900a0..a6523788cb 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 0aac601045..8d75ca3dbb 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -19,7 +19,7 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -43,21 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @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. - */ - public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @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 audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, audioProcessors); } @Override diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 7f1a790dad..1c23b9987c 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index 8e7f5e17d5..ee0a9fa5b5 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -5,7 +5,10 @@ native ; } -# Some members of this class are being accessed from native methods. Keep them unobfuscated. +# Some members of these classes are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } +-keep class com.google.android.exoplayer2.util.FlacStreamInfo { + *; +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 42c5908619..d13194793e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); try { decoderJni = new FlacDecoderJni(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index eb7206c9cf..246cde9d2f 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -38,21 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @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. - */ - public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @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 audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, audioProcessors); } @Override diff --git a/extensions/flac/src/main/jni/Android.mk b/extensions/flac/src/main/jni/Android.mk index e009333633..ff54c1b3c0 100644 --- a/extensions/flac/src/main/jni/Android.mk +++ b/extensions/flac/src/main/jni/Android.mk @@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \ LOCAL_SRC_FILES := $(FLAC_SOURCES) LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM -LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC +LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions LOCAL_LDLIBS := -llog -lz -lm diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 7d22c7fe79..e4925cb462 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) { } FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; - for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) { + for (unsigned i = mSeekTable->num_points; i > 0; ) { + i--; if (points[i].sample_number <= sample) { return firstFrameOffset + points[i].stream_offset; } diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md new file mode 100644 index 0000000000..bae5de4812 --- /dev/null +++ b/extensions/gvr/README.md @@ -0,0 +1,38 @@ +# ExoPlayer GVR Extension # + +## Description ## + +The GVR extension wraps the [Google VR SDK for Android][]. It provides a +GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering +of surround sound and ambisonic soundfields. + +## Using the extension ## + +The easiest way to use the extension is to add it as a gradle dependency. You +need to make sure you have the jcenter repository included in the `build.gradle` +file in the root of your project: + +```gradle +repositories { + jcenter() +} +``` + +Next, include the following in your module's `build.gradle` file: + +```gradle +compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +## Using GvrAudioProcessor ## + +* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to + return a GvrAudioProcessor. +* If constructing renderers directly, pass a GvrAudioProcessor to + MediaCodecAudioRenderer's constructor. + +[Google VR SDK for Android]: https://developers.google.com/vr/android/ +[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle new file mode 100644 index 0000000000..5156cf0540 --- /dev/null +++ b/extensions/gvr/build.gradle @@ -0,0 +1,35 @@ +// Copyright (C) 2017 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 19 + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(':library') + compile 'com.google.vr:sdk-audio:1.30.0' +} + +ext { + releaseArtifact = 'extension-gvr' + releaseDescription = 'Google VR extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/gvr/src/main/AndroidManifest.xml b/extensions/gvr/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6706b2507e --- /dev/null +++ b/extensions/gvr/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java new file mode 100644 index 0000000000..2117985da0 --- /dev/null +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 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.exoplayer2.ext.gvr; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.vr.sdk.audio.GvrAudioSurround; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of + * surround sound and ambisonic soundfields. + */ +public final class GvrAudioProcessor implements AudioProcessor { + + private static final int FRAMES_PER_OUTPUT_BUFFER = 1024; + private static final int OUTPUT_CHANNEL_COUNT = 2; + private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. + + private int sampleRateHz; + private int channelCount; + private GvrAudioSurround gvrAudioSurround; + private ByteBuffer buffer; + private boolean inputEnded; + + private float w; + private float x; + private float y; + private float z; + + /** + * Creates a new GVR audio processor. + */ + public GvrAudioProcessor() { + // Use the identity for the initial orientation. + w = 1f; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + } + + /** + * Updates the listener head orientation. May be called on any thread. See + * {@code GvrAudioSurround.updateNativeOrientation}. + */ + public synchronized void updateOrientation(float w, float x, float y, float z) { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + if (gvrAudioSurround != null) { + gvrAudioSurround.updateNativeOrientation(w, x, y, z); + } + } + + @Override + public synchronized boolean configure(int sampleRateHz, int channelCount, + @C.Encoding int encoding) throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_16BIT) { + maybeReleaseGvrAudioSurround(); + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + maybeReleaseGvrAudioSurround(); + int surroundFormat; + switch (channelCount) { + case 2: + surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; + break; + case 4: + surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS; + break; + case 6: + surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE; + break; + case 9: + surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS; + break; + case 16: + surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS; + break; + default: + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount, + FRAMES_PER_OUTPUT_BUFFER); + gvrAudioSurround.updateNativeOrientation(w, x, y, z); + if (buffer == null) { + buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) + .order(ByteOrder.nativeOrder()); + } + return true; + } + + @Override + public boolean isActive() { + return gvrAudioSurround != null; + } + + @Override + public int getOutputChannelCount() { + return OUTPUT_CHANNEL_COUNT; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer input) { + int position = input.position(); + int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position); + input.position(position + readBytes); + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + gvrAudioSurround.triggerProcessing(); + } + + @Override + public ByteBuffer getOutput() { + int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity()); + buffer.position(0).limit(writtenBytes); + return buffer; + } + + @Override + public boolean isEnded() { + return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0; + } + + @Override + public void flush() { + gvrAudioSurround.flush(); + inputEnded = false; + } + + @Override + public synchronized void release() { + buffer = null; + maybeReleaseGvrAudioSurround(); + } + + private void maybeReleaseGvrAudioSurround() { + if (this.gvrAudioSurround != null) { + GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround; + this.gvrAudioSurround = null; + gvrAudioSurround.release(); + } + } + +} diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index bbf69c60e4..3a2daefb8f 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -12,31 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. apply plugin: 'com.android.library' -apply plugin: 'bintray-release' android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } + + lintOptions { + // See: https://github.com/square/okio/issues/58 + warning 'InvalidPackage' + } } dependencies { compile project(':library') - compile('com.squareup.okhttp3:okhttp:3.4.1') { + compile('com.squareup.okhttp3:okhttp:3.6.0') { exclude group: 'org.json' } } -publish { - artifactId = 'extension-okhttp' - description = 'An OkHttp extension for ExoPlayer.' - repoName = releaseRepoName - userOrg = releaseUserOrg - groupId = releaseGroupId - version = releaseVersion - website = releaseWebsite +ext { + releaseArtifact = 'extension-okhttp' + releaseDescription = 'OkHttp extension for ExoPlayer.' } +apply from: '../../publish.gradle' diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 90a4728933..47850c0637 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -27,7 +27,6 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource { private final Predicate contentTypePredicate; private final TransferListener listener; private final CacheControl cacheControl; - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private DataSpec dataSpec; private Response response; @@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource { */ public OkHttpDataSource(Call.Factory callFactory, String userAgent, Predicate contentTypePredicate, TransferListener listener) { - this(callFactory, userAgent, contentTypePredicate, listener, null); + this(callFactory, userAgent, contentTypePredicate, listener, null, null); } /** @@ -99,16 +99,19 @@ public class OkHttpDataSource implements HttpDataSource { * {@link #open(DataSpec)}. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. + * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to + * the server as HTTP headers on every request. */ public OkHttpDataSource(Call.Factory callFactory, String userAgent, Predicate contentTypePredicate, TransferListener listener, - CacheControl cacheControl) { + CacheControl cacheControl, RequestProperties defaultRequestProperties) { this.callFactory = Assertions.checkNotNull(callFactory); this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; this.listener = listener; this.cacheControl = cacheControl; - this.requestProperties = new HashMap<>(); + this.defaultRequestProperties = defaultRequestProperties; + this.requestProperties = new RequestProperties(); } @Override @@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource { public void setRequestProperty(String name, String value) { Assertions.checkNotNull(name); Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource { if (cacheControl != null) { builder.cacheControl(cacheControl); } - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { - builder.addHeader(property.getKey(), property.getValue()); + if (defaultRequestProperties != null) { + for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { + builder.header(property.getKey(), property.getValue()); } } + for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + builder.header(property.getKey(), property.getValue()); + } if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index 8cbe295fa4..5228065db1 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.okhttp; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.TransferListener; @@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory { } @Override - protected OkHttpDataSource createDataSourceInternal() { - return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl); + protected OkHttpDataSource createDataSourceInternal( + HttpDataSource.RequestProperties defaultRequestProperties) { + return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl, + defaultRequestProperties); } } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index d354654c14..a6523788cb 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } @@ -32,4 +32,3 @@ android { dependencies { compile project(':library') } - diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 1850e68229..564a41fc77 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -40,35 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); + public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, audioProcessors); } /** * @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 audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); - } - - /** - * @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 audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - super(eventHandler, eventListener, audioCapabilities, drmSessionManager, - playClearSamplesWithoutKeys); + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, + audioProcessors); } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 6d0deb44ae..83e461d279 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -213,7 +213,7 @@ import java.util.List; SimpleOutputBuffer outputBuffer, int sampleRate); private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native void opusClose(long decoder); private native void opusReset(long decoder); diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 97c6b46280..90ded8fdc0 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \ git clone https://chromium.googlesource.com/libyuv/libyuv libyuv ``` +* Checkout the appropriate branches of libvpx and libyuv (the scripts and + makefiles bundled in this repo are known to work only at these versions of the + libraries - we will update this periodically as newer versions of + libvpx/libyuv are released): + +``` +cd "${VP9_EXT_PATH}/jni/libvpx" && \ +git checkout tags/v1.6.1 -b v1.6.1 && \ +cd "${VP9_EXT_PATH}/jni/libyuv" && \ +git checkout e2611a73 +``` + * Run a script that generates necessary configuration files for libvpx: ``` @@ -79,5 +91,7 @@ dependencies { `generate_libvpx_android_configs.sh` * Clean and re-build the project. * If you want to use your own version of libvpx or libyuv, place it in - `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. + `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But + please note that `generate_libvpx_android_configs.sh` and the makefiles need + to be modified to work with arbitrary versions of libvpx and libyuv. diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index d354654c14..91d80f4970 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm b/extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm new file mode 100644 index 0000000000..b3bd1b9d74 Binary files /dev/null and b/extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm differ diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index b1ddf2368c..f888554e22 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -19,6 +19,7 @@ import android.content.Context; import android.net.Uri; import android.os.Looper; import android.test.InstrumentationTestCase; +import android.util.Log; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -38,8 +39,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { private static final String BEAR_URI = "asset:///bear-vp9.webm"; private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm"; + private static final String ROADTRIP_10BIT_URI = "asset:///roadtrip-vp92-10bit.webm"; private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm"; + private static final String TAG = "VpxPlaybackTest"; + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_URI); } @@ -48,6 +52,15 @@ public class VpxPlaybackTest extends InstrumentationTestCase { playUri(BEAR_ODD_DIMENSIONS_URI); } + public void test10BitProfile2Playback() throws ExoPlaybackException { + if (VpxLibrary.isHighBitDepthSupported()) { + Log.d(TAG, "High Bit Depth supported."); + playUri(ROADTRIP_10BIT_URI); + return; + } + Log.d(TAG, "High Bit Depth not supported."); + } + public void testInvalidBitstream() { try { playUri(INVALID_BITSTREAM_URI); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index e4cc2ae3ce..d0417bc37e 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; private final FormatHolder formatHolder; + private final DecoderInputBuffer flagsOnlyBuffer; private final DrmSessionManager drmSessionManager; private DecoderCounters decoderCounters; @@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { joiningDeadlineMs = -1; clearLastReportedVideoSize(); formatHolder = new FormatHolder(); + flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; } @@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return; } - // Try and read a format if we don't have one already. - if (format == null && !readFormat()) { - // We can't make progress without one. - return; + if (format == null) { + // We don't have a format yet, so try and read one. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + outputStreamEnded = true; + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } } if (isRendererAvailable()) { @@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { // We've already read an encrypted sample into buffer, and are waiting for keys. result = C.RESULT_BUFFER_READ; } else { - result = readSource(formatHolder, inputBuffer); + result = readSource(formatHolder, inputBuffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -485,15 +499,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private boolean readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; - } - return false; - } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 0d7547d125..73ec7c2f96 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -141,7 +141,7 @@ import java.nio.ByteBuffer; private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); private native int vpxGetErrorCode(long context); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 2caa33c17c..24331127ec 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -57,6 +57,16 @@ public final class VpxLibrary { return isAvailable() ? vpxGetBuildConfig() : null; } + /** + * Returns true if the underlying libvpx library supports high bit depth. + */ + public static boolean isHighBitDepthSupported() { + String config = getBuildConfig(); + int indexHbd = config != null + ? config.indexOf("--enable-vp9-highbitdepth") : -1; + return indexHbd >= 0; + } + private static native String vpxGetVersion(); private static native String vpxGetBuildConfig(); public static native boolean vpxIsSecureDecodeSupported(); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index c76d0eda03..db3cf49b0c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -26,6 +26,7 @@ import java.nio.ByteBuffer; public static final int COLORSPACE_UNKNOWN = 0; public static final int COLORSPACE_BT601 = 1; public static final int COLORSPACE_BT709 = 2; + public static final int COLORSPACE_BT2020 = 3; private final VpxDecoder owner; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java index d108ae8b4f..837539593e 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java @@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10; 1.793f, -0.533f, 0.0f, }; + private static final float[] kColorConversion2020 = { + 1.168f, 1.168f, 1.168f, + 0.0f, -0.188f, 2.148f, + 1.683f, -0.652f, 0.0f, + }; + private static final String VERTEX_SHADER = "varying vec2 interp_tc;\n" + "attribute vec4 in_pos;\n" @@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10; + "uniform sampler2D v_tex;\n" + "uniform mat3 mColorConversion;\n" + "void main() {\n" - + " vec3 yuv;" + + " vec3 yuv;\n" + " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n" + " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n" + " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n" - + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);" + + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n" + "}\n"; + private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer( -1.0f, 1.0f, -1.0f, -1.0f, @@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10; } VpxOutputBuffer outputBuffer = renderedOutputBuffer; // Set color matrix. Assume BT709 if the color space is unknown. - float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601 - ? kColorConversion601 : kColorConversion709; + float[] colorConversion = kColorConversion709; + switch (outputBuffer.colorspace) { + case VpxOutputBuffer.COLORSPACE_BT601: + colorConversion = kColorConversion601; + break; + case VpxOutputBuffer.COLORSPACE_BT2020: + colorConversion = kColorConversion2020; + break; + case VpxOutputBuffer.COLORSPACE_BT709: + default: + break; // Do nothing + } GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); for (int i = 0; i < 3; i++) { diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index f0fb2761db..566396e0bf 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm" arch[1]="armeabi" config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" -config[1]+=" --disable-neon-asm --disable-media" +config[1]+=" --disable-neon-asm" arch[2]="mips" config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" @@ -78,12 +78,12 @@ convert_asm() { for i in $(seq 0 ${limit}); do while read file; do case "${file}" in - *.asm.s) + *.asm.[sS]) # Some files may already have been processed (there are duplicated # .asm.s files for vp8 in the armeabi/armeabi-v7a configurations). file="libvpx/${file}" if [[ ! -e "${file}" ]]; then - asm_file="${file%.s}" + asm_file="${file%.[sS]}" cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}" remove_trailing_whitespace "${file}" rm "${asm_file}" @@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do echo "configure ${config[${i}]} ${common_params}" ../../libvpx/configure ${config[${i}]} ${common_params} rm -f libvpx_srcs.txt - make libvpx_srcs.txt + for f in ${allowed_files}; do + # the build system supports multiple different configurations. avoid + # failing out when, for example, vp8_rtcd.h is not part of a configuration + make "${f}" || true + done # remove files that aren't needed rm -rf !(${allowed_files// /|}) diff --git a/extensions/vp9/src/main/jni/libvpx.mk b/extensions/vp9/src/main/jni/libvpx.mk index 6cc706ffa8..887de56218 100644 --- a/extensions/vp9/src/main/jni/libvpx.mk +++ b/extensions/vp9/src/main/jni/libvpx.mk @@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \ $(filter %.c, $(libvpx_codec_srcs)))) # include assembly files if they exist -# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly +# "%.asm.[sS]" covers neon assembly and "%.asm" covers x86 assembly LOCAL_SRC_FILES += $(addprefix libvpx/, \ $(filter %.asm.s %.asm, $(libvpx_codec_srcs))) +LOCAL_SRC_FILES += $(addprefix libvpx/, \ + $(filter %.asm.S %.asm, $(libvpx_codec_srcs))) ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) -# append .neon to *_neon.c and *.s +# append .neon to *_neon.c and *.[sS] LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES)) LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES)) +LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES)) endif +# remove duplicates +LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES)) + LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \ $(LOCAL_PATH)/libvpx/vpx diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 137ff9ac21..c091091389 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) { vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); errorCode = 0; - if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { - LOGE("ERROR: Fail to initialize libvpx decoder."); + vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, + &cfg, 0); + if (err) { + LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err); + errorCode = err; return 0; } @@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int kColorspaceUnknown = 0; const int kColorspaceBT601 = 1; const int kColorspaceBT709 = 2; + const int kColorspaceBT2020 = 3; int colorspace = kColorspaceUnknown; switch (img->cs) { @@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { case VPX_CS_BT_709: colorspace = kColorspaceBT709; break; + case VPX_CS_BT_2020: + colorspace = kColorspaceBT2020; + break; default: break; } @@ -186,14 +193,55 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { jbyte* const data = reinterpret_cast(env->GetDirectBufferAddress(dataObject)); - // TODO: This copy can be eliminated by using external frame buffers. NOLINT - // This is insignificant for smaller videos but takes ~1.5ms for 1080p - // clips. So this should eventually be gotten rid of. - const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h; - const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2); - memcpy(data, img->planes[VPX_PLANE_Y], y_length); - memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length); - memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length); + const int32_t uvHeight = (img->d_h + 1) / 2; + const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h; + const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight; + int sample = 0; + if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420. + // Note: The stride for BT2020 is twice of what we use so this is wasting + // memory. The long term goal however is to upload half-float/short so + // it's not important to optimize the stride at this time. + // Y + for (int y = 0; y < img->d_h; y++) { + const uint16_t* srcBase = reinterpret_cast( + img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); + int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y; + for (int x = 0; x < img->d_w; x++) { + // Lightweight dither. Carryover the remainder of each 10->8 bit + // conversion to the next pixel. + sample += *srcBase++; + *destBase++ = sample >> 2; + sample = sample & 3; // Remainder. + } + } + // UV + const int32_t uvWidth = (img->d_w + 1) / 2; + for (int y = 0; y < uvHeight; y++) { + const uint16_t* srcUBase = reinterpret_cast( + img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y); + const uint16_t* srcVBase = reinterpret_cast( + img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y); + int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y; + int8_t* destVBase = data + yLength + uvLength + + img->stride[VPX_PLANE_V] * y; + for (int x = 0; x < uvWidth; x++) { + // Lightweight dither. Carryover the remainder of each 10->8 bit + // conversion to the next pixel. + sample += *srcUBase++; + *destUBase++ = sample >> 2; + sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder. + *destVBase++ = sample >> 2; + sample = sample & 3; // Remainder. + } + } + } else { + // TODO: This copy can be eliminated by using external frame buffers. This + // is insignificant for smaller videos but takes ~1.5ms for 1080p clips. + // So this should eventually be gotten rid of. + memcpy(data, img->planes[VPX_PLANE_Y], yLength); + memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength); + memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength); + } } return 0; } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c41838fae2..8c0a9b91f6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Oct 24 14:40:37 BST 2016 +#Mon Mar 13 11:17:14 GMT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/library/build.gradle b/library/build.gradle index 0d4bbd0256..abca404cfa 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -14,19 +14,13 @@ import com.android.builder.core.BuilderConstants apply plugin: 'com.android.library' -apply plugin: 'bintray-release' android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion defaultConfig { - // Important: ExoPlayerLib specifies a minSdkVersion of 9 because - // various components provided by the library may be of use on older - // devices. However, please note that the core video playback - // functionality provided by the library requires API level 16 or - // greater. - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } @@ -47,10 +41,10 @@ android { } dependencies { + compile 'com.android.support:support-annotations:25.2.0' androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'org.mockito:mockito-core:1.9.5' - compile 'com.android.support:support-annotations:25.0.1' } android.libraryVariants.all { variant -> @@ -86,12 +80,8 @@ android.libraryVariants.all { variant -> } } -publish { - artifactId = 'exoplayer' - description = 'The ExoPlayer library.' - repoName = releaseRepoName - userOrg = releaseUserOrg - groupId = releaseGroupId - version = releaseVersion - website = releaseWebsite +ext { + releaseArtifact = 'exoplayer' + releaseDescription = 'The ExoPlayer library.' } +apply from: '../publish.gradle' diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 new file mode 100644 index 0000000000..16907fdd98 Binary files /dev/null and b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 differ diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump new file mode 100644 index 0000000000..9d3755b23b --- /dev/null +++ b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -0,0 +1,382 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = 0 +numberOfTracks = 3 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = video/avc + maxInputSize = -1 + width = 1080 + height = 720 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample count = 30 + sample 0: + time = 66000 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 199000 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 132000 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100000 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166000 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 332000 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266000 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233000 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 299000 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 466000 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 399000 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367000 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433000 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 599000 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533000 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500000 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 566000 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 733000 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 666000 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633000 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700000 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 866000 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800000 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767000 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 833000 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 933000 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900000 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967000 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1033000 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + format: + bitrate = -1 + id = 2 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = -1 + pixelWidthHeightRatio = -1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + data = length 5, hash 2B7623A + sample count = 46 + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23000 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46000 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69000 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92000 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116000 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139000 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162000 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185000 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208000 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232000 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255000 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278000 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301000 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325000 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348000 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371000 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394000 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417000 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441000 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464000 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487000 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510000 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534000 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557000 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580000 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603000 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626000 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650000 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673000 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696000 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719000 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743000 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766000 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789000 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812000 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835000 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859000 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882000 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905000 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928000 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952000 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975000 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998000 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021000 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044000 + flags = 1 + data = length 10, hash A453EEBE +track 3: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = -1 + pixelWidthHeightRatio = -1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + sample count = 0 +tracksEnded = true diff --git a/library/src/androidTest/assets/ts/sample.ac3.0.dump b/library/src/androidTest/assets/ts/sample.ac3.0.dump index c5f241950b..1b6c77efb6 100644 --- a/library/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/src/androidTest/assets/ts/sample.ac3.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 1 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/ac3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.adts.0.dump b/library/src/androidTest/assets/ts/sample.adts.0.dump index 3325abcfeb..0a7427d3f1 100644 --- a/library/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/src/androidTest/assets/ts/sample.adts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/mp4a-latm maxInputSize = -1 @@ -606,7 +606,7 @@ track 0: track 1: format: bitrate = -1 - id = null + id = 1 containerMimeType = null sampleMimeType = application/id3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ps.0.dump b/library/src/androidTest/assets/ts/sample.ps.0.dump index 48127ce1c6..3b44fb6fb9 100644 --- a/library/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/src/androidTest/assets/ts/sample.ps.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 192: format: bitrate = -1 - id = null + id = 192 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 @@ -45,7 +45,7 @@ track 192: track 224: format: bitrate = -1 - id = null + id = 224 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ts.0.dump b/library/src/androidTest/assets/ts/sample.ts.0.dump index 8b0da7bd02..26c6665aaa 100644 --- a/library/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/src/androidTest/assets/ts/sample.ts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 256: format: bitrate = -1 - id = null + id = 1/256 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 @@ -38,7 +38,7 @@ track 256: track 257: format: bitrate = -1 - id = null + id = 1/257 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 1197139b01..93c0a7dc11 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -460,6 +460,11 @@ public final class ExoPlayerTest extends TestCase { return 0; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public long readDiscontinuity() { assertTrue(preparedPeriod); @@ -513,8 +518,9 @@ public final class ExoPlayerTest extends TestCase { } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (buffer == null || !readFormat) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (formatRequired || !readFormat) { formatHolder.format = format; readFormat = true; return C.RESULT_FORMAT_READ; @@ -571,7 +577,7 @@ public final class ExoPlayerTest extends TestCase { FormatHolder formatHolder = new FormatHolder(); DecoderInputBuffer buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - int result = readSource(formatHolder, buffer); + int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_FORMAT_READ) { formatReadCount++; assertEquals(expectedFormat, formatHolder.format); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 9eed8dfd3a..985e93404a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.HashMap; import org.mockito.Mock; @@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static Representation newRepresentations(DrmInitData drmInitData) { - Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData); + Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); + if (drmInitData != null) { + format = format.copyWithDrmInitData(drmInitData); + } return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 9a8a1f7f27..95ad8b446e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -25,21 +25,32 @@ import com.google.android.exoplayer2.testutil.TestUtil; */ public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { - private static final TestUtil.ExtractorFactory EXTRACTOR_FACTORY = - new TestUtil.ExtractorFactory() { - @Override - public Extractor create() { - return new FragmentedMp4Extractor(); - } - }; - public void testSample() throws Exception { - TestUtil.assertOutput(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation()); + TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); + } + + public void testSampleWithSeiPayloadParsing() throws Exception { + // Enabling the CEA-608 track enables SEI payload parsing. + TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), + "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } public void testAtomWithZeroSize() throws Exception { - TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4", + TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", getInstrumentation(), ParserException.class); } + private static TestUtil.ExtractorFactory getExtractorFactory() { + return getExtractorFactory(0); + } + + private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) { + return new TestUtil.ExtractorFactory() { + @Override + public Extractor create() { + return new FragmentedMp4Extractor(flags, null); + } + }; + } + } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index ebb547810b..bcfa90a565 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - adtsOutput = fakeExtractorOutput.track(0); - id3Output = fakeExtractorOutput.track(1); + adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); + id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); adtsReader = new AdtsReader(true); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); adtsReader.createTracks(fakeExtractorOutput, idGenerator); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 2dce742158..7bf722cd8f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import android.util.SparseArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -74,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomPesReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); - TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setSimulateIOErrors(false) @@ -92,13 +94,14 @@ public final class TsExtractorTest extends InstrumentationTestCase { TrackOutput trackOutput = reader.getTrackOutput(); assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)); assertEquals( - Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0), + Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0), ((FakeTrackOutput) trackOutput).format); } public void testCustomInitialSectionReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); - TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) .setSimulateIOErrors(false) @@ -178,8 +181,9 @@ public final class TsExtractorTest extends InstrumentationTestCase { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java new file mode 100644 index 0000000000..c796025b08 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.dash.manifest; + +import android.net.Uri; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import junit.framework.TestCase; + +/** + * Unit tests for {@link DashManifest}. + */ +public class DashManifestTest extends TestCase { + + private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); + private static final List DUMMY_ACCESSIBILITY_DESCRIPTORS = + Collections.emptyList(); + private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); + private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); + + public void testCopy() throws Exception { + Representation[][][] representations = newRepresentations(3, 2, 3); + DashManifest sourceManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0]), + newAdaptationSet(3, representations[0][1])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]), + newAdaptationSet(6, representations[1][1])), + newPeriod("7", 7, + newAdaptationSet(8, representations[2][0]), + newAdaptationSet(9, representations[2][1]))); + + List keys = Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 0, 1), + new RepresentationKey(0, 1, 2), + + new RepresentationKey(1, 0, 1), + new RepresentationKey(1, 1, 0), + new RepresentationKey(1, 1, 2), + + new RepresentationKey(2, 0, 1), + new RepresentationKey(2, 0, 2), + new RepresentationKey(2, 1, 0)); + // Keys don't need to be in any particular order + Collections.shuffle(keys, new Random(0)); + + DashManifest copyManifest = sourceManifest.copy(keys); + + DashManifest expectedManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), + newAdaptationSet(3, representations[0][1][2])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0][1]), + newAdaptationSet(6, representations[1][1][0], representations[1][1][2])), + newPeriod("7", 7, + newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), + newAdaptationSet(9, representations[2][1][0]))); + assertManifestEquals(expectedManifest, copyManifest); + } + + public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception { + Representation[][][] representations = newRepresentations(2, 1, 1); + DashManifest sourceManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]))); + + DashManifest copyManifest = sourceManifest.copy(Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(1, 0, 0))); + + DashManifest expectedManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]))); + assertManifestEquals(expectedManifest, copyManifest); + } + + public void testCopySkipPeriod() throws Exception { + Representation[][][] representations = newRepresentations(3, 2, 3); + DashManifest sourceManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0]), + newAdaptationSet(3, representations[0][1])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]), + newAdaptationSet(6, representations[1][1])), + newPeriod("7", 7, + newAdaptationSet(8, representations[2][0]), + newAdaptationSet(9, representations[2][1]))); + + DashManifest copyManifest = sourceManifest.copy(Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 0, 1), + new RepresentationKey(0, 1, 2), + + new RepresentationKey(2, 0, 1), + new RepresentationKey(2, 0, 2), + new RepresentationKey(2, 1, 0))); + + DashManifest expectedManifest = newDashManifest(7, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), + newAdaptationSet(3, representations[0][1][2])), + newPeriod("7", 4, + newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), + newAdaptationSet(9, representations[2][1][0]))); + assertManifestEquals(expectedManifest, copyManifest); + } + + private static void assertManifestEquals(DashManifest expected, DashManifest actual) { + assertEquals(expected.availabilityStartTime, actual.availabilityStartTime); + assertEquals(expected.duration, actual.duration); + assertEquals(expected.minBufferTime, actual.minBufferTime); + assertEquals(expected.dynamic, actual.dynamic); + assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod); + assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth); + assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay); + assertEquals(expected.utcTiming, actual.utcTiming); + assertEquals(expected.location, actual.location); + assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); + for (int i = 0; i < expected.getPeriodCount(); i++) { + Period expectedPeriod = expected.getPeriod(i); + Period actualPeriod = actual.getPeriod(i); + assertEquals(expectedPeriod.id, actualPeriod.id); + assertEquals(expectedPeriod.startMs, actualPeriod.startMs); + List expectedAdaptationSets = expectedPeriod.adaptationSets; + List actualAdaptationSets = actualPeriod.adaptationSets; + assertEquals(expectedAdaptationSets.size(), actualAdaptationSets.size()); + for (int j = 0; j < expectedAdaptationSets.size(); j++) { + AdaptationSet expectedAdaptationSet = expectedAdaptationSets.get(j); + AdaptationSet actualAdaptationSet = actualAdaptationSets.get(j); + assertEquals(expectedAdaptationSet.id, actualAdaptationSet.id); + assertEquals(expectedAdaptationSet.type, actualAdaptationSet.type); + assertEquals(expectedAdaptationSet.accessibilityDescriptors, + actualAdaptationSet.accessibilityDescriptors); + assertEquals(expectedAdaptationSet.representations, actualAdaptationSet.representations); + } + } + } + + private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts, + int representationCounts) { + Representation[][][] representations = new Representation[periodCount][][]; + for (int i = 0; i < periodCount; i++) { + representations[i] = new Representation[adaptationSetCounts][]; + for (int j = 0; j < adaptationSetCounts; j++) { + representations[i][j] = new Representation[representationCounts]; + for (int k = 0; k < representationCounts; k++) { + representations[i][j][k] = newRepresentation(); + } + } + } + return representations; + } + + private static Representation newRepresentation() { + return Representation.newInstance("", 0, DUMMY_FORMAT, "", DUMMY_SEGMENT_BASE); + } + + private static DashManifest newDashManifest(int duration, Period... periods) { + return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY, + Arrays.asList(periods)); + } + + private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) { + return new Period(id, startMs, Arrays.asList(adaptationSets)); + } + + private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { + return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), + DUMMY_ACCESSIBILITY_DESCRIPTORS); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index f0adf274ee..aa279f23f4 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -19,6 +19,7 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; @@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; - public void testParseMasterPlaylist() throws IOException{ - HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST); - assertNotNull(playlist); - assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); + private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + public void testParseMasterPlaylist() throws IOException{ + HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; assertNotNull(variants); @@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase { public void testPlaylistWithInvalidHeader() throws IOException { try { - parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); fail("Expected exception not thrown."); } catch (ParserException e) { // Expected due to invalid header. } } - private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException { + public void testPlaylistWithClosedCaption() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC); + assertEquals(1, playlist.muxedCaptionFormats.size()); + Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); + assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType); + assertEquals(4, closedCaptionFormat.accessibilityChannel); + assertEquals("es", closedCaptionFormat.language); + } + + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) + throws IOException { Uri playlistUri = Uri.parse(uri); ByteArrayInputStream inputStream = new ByteArrayInputStream( playlistString.getBytes(Charset.forName(C.UTF8_NAME))); - return new HlsPlaylistParser().parse(playlistUri, inputStream); + return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 4286a283c0..3d976353cc 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -36,6 +36,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { String playlistString = "#EXTM3U\n" + "#EXT-X-VERSION:3\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-START:TIME-OFFSET=-25" + "#EXT-X-TARGETDURATION:8\n" + "#EXT-X-MEDIA-SEQUENCE:2679\n" + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" @@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType); + assertEquals(mediaPlaylist.durationUs - 25000000, mediaPlaylist.startOffsetUs); assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(3, mediaPlaylist.version); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 067cfe4fcd..6689d73ff1 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testMaxCacheFileSize() throws Exception { @@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase { MoreAsserts.assertEmpty(simpleCache.getKeys()); } + public void testReadOnlyCache() throws Exception { + CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); + assertReadDataContentLength(cacheDataSource, false, false); + assertEquals(0, cacheDir.list().length); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { - // Read all data from upstream and cache + // Read all data from upstream and write to cache CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength); assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength); @@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { - Builder builder = new Builder(); + return createCacheDataSource(setReadException, simulateUnknownLength, flags, + new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); + } + + private CacheDataSource createCacheDataSource(boolean setReadException, + boolean simulateUnknownLength, @CacheDataSource.Flags int flags, + CacheDataSink cacheWriteDataSink) { + FakeDataSource.Builder builder = new FakeDataSource.Builder(); if (setReadException) { builder.appendReadError(new IOException("Shouldn't read from upstream")); } - builder.setSimulateUnknownLength(simulateUnknownLength); - builder.appendReadData(TEST_DATA); - FakeDataSource upstream = builder.build(); - return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE); + FakeDataSource upstream = + builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build(); + return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, + flags, null); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 4fbcc92e3d..7f6e203c20 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import android.util.SparseArray; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testAddGetRemove() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 799027f4b5..f2e199578c 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; @@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testGetRegion_noSpansInCache() { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 0b40cd7735..8c684b1cb3 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testCacheFile() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 001c6adc87..1a6beeb6ba 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileInputStream; @@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testCommittingOneFile() throws Exception { @@ -192,6 +191,41 @@ public class SimpleCacheTest extends InstrumentationTestCase { assertEquals(0, cacheDir.listFiles().length); } + + public void testGetCachedBytes() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); + + // No cached bytes, returns -'length' + assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + // Position value doesn't affect the return value + assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100)); + + addCache(simpleCache, KEY_1, 0, 15); + + // Returns the length of a single span + assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + // Value is capped by the 'length' + assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10)); + + addCache(simpleCache, KEY_1, 15, 35); + + // Returns the length of two adjacent spans + assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + addCache(simpleCache, KEY_1, 60, 10); + + // Not adjacent span doesn't affect return value + assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + // Returns length of hole up to the next cached span + assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100)); + + simpleCache.releaseHoleSpan(cacheSpan); + } + private SimpleCache getSimpleCache() { return new SimpleCache(cacheDir, new NoOpCacheEvictor()); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java index 7cdbb9a5b1..6c5d7c76f7 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.testutil.TestUtil; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { - tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext()); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); file = new File(tempFolder, "atomicFile"); atomicFile = new AtomicFile(file); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(tempFolder); + Util.recursiveDelete(tempFolder); } public void testDelete() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 35e168e514..923d1d8aaa 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -142,8 +142,10 @@ public class UtilTest extends TestCase { public void testParseXsDateTime() throws Exception { assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); + assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); + assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800")); } public void testUnescapeInvalidFileName() { diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 9973a50cff..f65be3afcd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -254,6 +254,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Use {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} instead. + */ + @Deprecated + protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) { + return readSource(formatHolder, buffer, false); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -262,13 +270,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the - * caller requires that the format of the stream be read even if it's not changing. + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) { - int result = stream.readData(formatHolder, buffer); + protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { readEndOfStream = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 0b1c33bfc9..6a1db191a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -443,9 +443,16 @@ public final class C { */ public static final UUID UUID_NIL = new UUID(0L, 0L); + /** + * UUID for the ClearKey DRM scheme. + *

+ * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. + */ + public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + /** * UUID for the Widevine DRM scheme. - *

+ *

* Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. */ public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); @@ -477,7 +484,7 @@ public final class C { * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the * underlying {@link android.media.AudioTrack}. The message object should not be modified by the - * caller after it has been passed + * caller after it has been passed. */ public static final int MSG_SET_PLAYBACK_PARAMS = 3; @@ -515,7 +522,13 @@ public final class C { * The stereo mode for 360/3D/VR videos. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT}) + @IntDef({ + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH + }) public @interface StereoMode {} /** * Indicates Monoscopic stereo layout, used with 360/3D/VR videos. @@ -529,6 +542,16 @@ public final class C { * Indicates Left-Right stereo layout, used with 360/3D/VR videos. */ public static final int STEREO_MODE_LEFT_RIGHT = 2; + /** + * Indicates a stereo layout where the left and right eyes have separate meshes, + * used with 360/3D/VR videos. + */ + public static final int STEREO_MODE_STEREO_MESH = 3; + + /** + * Priority for media playback. + */ + public static final int PRIORITY_PLAYBACK = 0; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving diff --git a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index fe7015a942..d8bc042ad7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,11 +51,6 @@ public final class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - /** - * Priority for media loading. - */ - public static final int LOADING_PRIORITY = 0; - private static final int ABOVE_HIGH_WATERMARK = 0; private static final int BETWEEN_WATERMARKS = 1; private static final int BELOW_LOW_WATERMARK = 2; @@ -122,7 +117,7 @@ public final class DefaultLoadControl implements LoadControl { * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining + * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining * periods. */ public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, @@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl { || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { - priorityTaskManager.add(LOADING_PRIORITY); + priorityTaskManager.add(C.PRIORITY_PLAYBACK); } else { - priorityTaskManager.remove(LOADING_PRIORITY); + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); } } return isBuffering; @@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl { private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { - priorityTaskManager.remove(LOADING_PRIORITY); + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); } isBuffering = false; if (resetAllocator) { diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 72ac72e981..ca7367f1b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception { * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and * {@link #TYPE_UNEXPECTED}. */ - @Type - public final int type; + @Type public final int type; /** * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer. diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index faf86087c9..e4c109e85b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -455,6 +455,8 @@ import java.io.IOException; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs); + boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (Renderer renderer : enabledRenderers) { diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 5100acbbd8..5ec7fac5dd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.2.0"; + String VERSION = "2.3.0"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2002000; + int VERSION_INT = 2003000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 0b558153fd..866e512288 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -120,7 +120,7 @@ public final class Format implements Parcelable { /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}. + * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. */ @C.StereoMode public final int stereoMode; @@ -438,16 +438,19 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithManifestFormatInfo(Format manifestFormat, - boolean preferManifestDrmInitData) { + public Format copyWithManifestFormatInfo(Format manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } String id = manifestFormat.id; String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null) - || this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData; + DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData + : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, @@ -672,9 +675,6 @@ public final class Format implements Parcelable { dest.writeParcelable(metadata, 0); } - /** - * {@link Creator} implementation. - */ public static final Creator CREATOR = new Creator() { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 298e528246..3ce4937911 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -28,6 +28,7 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; @@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, componentListener, allowedVideoJoiningTimeMs, out); buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, out); + componentListener, buildAudioProcessors(), out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); @@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers @@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers + * before output. May be empty. * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, Handler mainHandler, DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - ArrayList out) { + AudioProcessor[] audioProcessors, ArrayList out) { out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); + mainHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, AudioProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { @@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, AudioProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { @@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, AudioProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { @@ -787,6 +793,13 @@ public class SimpleExoPlayer implements ExoPlayer { // Do nothing. } + /** + * Builds an array of {@link AudioProcessor}s that will process PCM audio before output. + */ + protected AudioProcessor[] buildAudioProcessors() { + return new AudioProcessor[0]; + } + // Internal methods. private void removeSurfaceCallbacks() { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java new file mode 100644 index 0000000000..2e0d1f98d9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 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.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Interface for audio processors. + */ +public interface AudioProcessor { + + /** + * Exception thrown when a processor can't be configured for a given input audio format. + */ + final class UnhandledFormatException extends Exception { + + public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { + super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " + + encoding); + } + + } + + /** + * An empty, direct {@link ByteBuffer}. + */ + ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); + + /** + * Configures the processor to process input audio with the specified format. After calling this + * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the + * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the + * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a + * result of the call. If it's active, {@link #getOutputChannelCount()} and + * {@link #getOutputEncoding()} return the processor's output format. + * + * @param sampleRateHz The sample rate of input audio in Hz. + * @param channelCount The number of interleaved channels in input audio. + * @param encoding The encoding of input audio. + * @return {@code true} if the processor must be flushed or the value returned by + * {@link #isActive()} has changed as a result of the call. + * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + */ + boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException; + + /** + * Returns whether the processor is configured and active. + */ + boolean isActive(); + + /** + * Returns the number of audio channels in the data output by the processor. + */ + int getOutputChannelCount(); + + /** + * Returns the audio encoding used in the data output by the processor. + */ + @C.Encoding + int getOutputEncoding(); + + /** + * Queues audio data between the position and limit of the input {@code buffer} for processing. + * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as + * read-only. Its position will be advanced by the number of bytes consumed (which may be zero). + * The caller retains ownership of the provided buffer. Calling this method invalidates any + * previous buffer returned by {@link #getOutput()}. + * + * @param buffer The input buffer to process. + */ + void queueInput(ByteBuffer buffer); + + /** + * Queues an end of stream signal. After this method has been called, + * {@link #queueInput(ByteBuffer)} may not be called until after the next call to + * {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple + * calls may be required to read all of the remaining output data. {@link #isEnded()} will return + * {@code true} once all remaining output data has been read. + */ + void queueEndOfStream(); + + /** + * Returns a buffer containing processed output data between its position and limit. The buffer + * will always be a direct byte buffer with native byte order. Calling this method invalidates any + * previously returned buffer. The buffer will be empty if no output is available. + * + * @return A buffer containing processed output data between its position and limit. + */ + ByteBuffer getOutput(); + + /** + * Returns whether this processor will return no more output from {@link #getOutput()} until it + * has been {@link #flush()}ed and more input has been queued. + */ + boolean isEnded(); + + /** + * Clears any state in preparation for receiving a new stream of input buffers. + */ + void flush(); + + /** + * Releases any resources associated with this instance. + */ + void release(); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 71049c9de8..3b8a1b8f39 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -25,13 +25,13 @@ import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; /** * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles @@ -54,8 +54,8 @@ import java.nio.ByteOrder; * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling * {@link #configure(String, int, int, int, int)}. *

- * Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be - * provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call + * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will + * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call * {@link #release()} when the instance is no longer required. */ public final class AudioTrack { @@ -91,6 +91,21 @@ public final class AudioTrack { } + /** + * Thrown when a failure occurs configuring the track. + */ + public static final class ConfigurationException extends Exception { + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message) { + super(message); + } + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -255,6 +270,8 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final AudioProcessor[] availableAudioProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -268,14 +285,13 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; + @C.Encoding + private int encoding; + @C.Encoding + private int outputEncoding; @C.StreamType private int streamType; - @C.Encoding - private int sourceEncoding; - @C.Encoding - private int targetEncoding; private boolean passthrough; - private int pcmFrameSize; private int bufferSize; private long bufferSizeUs; @@ -290,8 +306,12 @@ public final class AudioTrack { private long lastTimestampSampleTimeUs; private Method getLatencyMethod; + private int pcmFrameSize; private long submittedPcmBytes; private long submittedEncodedFrames; + private int outputPcmFrameSize; + private long writtenPcmBytes; + private long writtenEncodedFrames; private int framesPerEncodedSample; private int startMediaTimeState; private long startMediaTimeUs; @@ -299,12 +319,14 @@ public final class AudioTrack { private long latencyUs; private float volume; - private byte[] temporaryBuffer; - private int temporaryBufferOffset; - private ByteBuffer currentSourceBuffer; - - private ByteBuffer resampledBuffer; - private boolean useResampledBuffer; + private AudioProcessor[] audioProcessors; + private ByteBuffer[] outputBuffers; + private ByteBuffer inputBuffer; + private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; + private int drainingAudioProcessorIndex; + private boolean handledEndOfStream; private boolean playing; private int audioSessionId; @@ -313,11 +335,20 @@ public final class AudioTrack { private long lastFeedElapsedRealtimeMs; /** - * @param audioCapabilities The current audio capabilities. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, + Listener listener) { this.audioCapabilities = audioCapabilities; + channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + availableAudioProcessors = new AudioProcessor[audioProcessors.length + 2]; + availableAudioProcessors[0] = new ResamplingAudioProcessor(); + availableAudioProcessors[1] = channelMappingAudioProcessor; + System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -340,6 +371,9 @@ public final class AudioTrack { startMediaTimeState = START_NOT_SET; streamType = C.STREAM_TYPE_DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; + drainingAudioProcessorIndex = C.INDEX_UNSET; + this.audioProcessors = new AudioProcessor[0]; + outputBuffers = new ByteBuffer[0]; } /** @@ -414,9 +448,70 @@ public final class AudioTrack { * {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. + * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null); + } + + /** + * Configures (or reconfigures) the audio track. + * + * @param mimeType The mime type. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size automatically. + * @param outputChannels A mapping from input to output channels that is applied to this track's + * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the + * input unchanged. Otherwise, the element at index {@code i} specifies index of the input + * channel to map to output channel {@code i} when preprocessing input buffers. After the + * map is applied the audio data will have {@code outputChannels.length} channels. + * @throws ConfigurationException If an error occurs configuring the track. + */ + public void configure(String mimeType, int channelCount, int sampleRate, + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels) + throws ConfigurationException { + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + boolean flush = false; + if (!passthrough) { + pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); + + // Reconfigure the audio processors. + channelMappingAudioProcessor.setChannelMap(outputChannels); + ArrayList newAudioProcessors = new ArrayList<>(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + try { + flush |= audioProcessor.configure(sampleRate, channelCount, encoding); + } catch (AudioProcessor.UnhandledFormatException e) { + throw new ConfigurationException(e); + } + if (audioProcessor.isActive()) { + newAudioProcessors.add(audioProcessor); + channelCount = audioProcessor.getOutputChannelCount(); + encoding = audioProcessor.getOutputEncoding(); + } else { + audioProcessor.flush(); + } + } + + if (flush) { + int count = newAudioProcessors.size(); + audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); + outputBuffers = new ByteBuffer[count]; + for (int i = 0; i < count; i++) { + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } + } + } + int channelConfig; switch (channelCount) { case 1: @@ -444,7 +539,7 @@ public final class AudioTrack { channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; break; default: - throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + throw new ConfigurationException("Unsupported channel count: " + channelCount); } // Workaround for overly strict channel configuration checks on nVidia Shield. @@ -462,25 +557,13 @@ public final class AudioTrack { } } - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int sourceEncoding; - if (passthrough) { - sourceEncoding = getEncodingForMimeType(mimeType); - } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT - || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - sourceEncoding = pcmEncoding; - } else { - throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); - } - - if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -488,38 +571,38 @@ public final class AudioTrack { reset(); - this.sourceEncoding = sourceEncoding; + this.encoding = encoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; - pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. + outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; + outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (passthrough) { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] - if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { + if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ { + } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize); + durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize : multipliedBufferSize; } - bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize)); + bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); } private void initialize() throws InitializationException { @@ -531,15 +614,15 @@ public final class AudioTrack { releasingConditionVariable.block(); if (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, bufferSize, audioSessionId); } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM); + outputEncoding, bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM, audioSessionId); + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); } checkAudioTrackInitialized(); @@ -594,25 +677,26 @@ public final class AudioTrack { } /** - * Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current - * position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is - * advanced by the number of bytes that were successfully written. - * {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is - * discontinuous with the last buffer handled since the track was reset. + * Attempts to process data from a {@link ByteBuffer}, starting from its current position and + * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the + * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if + * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. *

- * Returns whether the data was written in full. If the data was not written in full then the same + * Returns whether the data was handled in full. If the data was not handled in full then the same * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to * {@link #configure(String, int, int, int, int)} that caused the track to be reset). * - * @param buffer The buffer containing audio data to play back. - * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. - * @return Whether the buffer was consumed fully. + * @param buffer The buffer containing audio data. + * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. + * @return Whether the buffer was handled fully. * @throws InitializationException If an error occurs initializing the track. * @throws WriteException If an error occurs writing the audio data. */ + @SuppressWarnings("ReferenceEquality") public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (!isInitialized()) { initialize(); if (playing) { @@ -620,27 +704,12 @@ public final class AudioTrack { } } - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); - } - boolean result = writeBuffer(buffer, presentationTimeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - return result; - } - - @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean isNewSourceBuffer = currentSourceBuffer == null; - Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); - currentSourceBuffer = buffer; - if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; return false; } @@ -653,27 +722,25 @@ public final class AudioTrack { } } - if (isNewSourceBuffer) { - // We're seeing this buffer for the first time. + boolean hadData = hasData; + hasData = hasPendingData(); + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + } - if (!currentSourceBuffer.hasRemaining()) { + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { // The buffer is empty. - currentSourceBuffer = null; return true; } - useResampledBuffer = targetEncoding != sourceEncoding; - if (useResampledBuffer) { - Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); - // Resample the buffer to get the data in the target encoding. - resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer); - buffer = resampledBuffer; - } - if (passthrough && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); + framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } + if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); startMediaTimeState = START_IN_SYNC; @@ -695,66 +762,172 @@ public final class AudioTrack { listener.onPositionDiscontinuity(); } } - if (Util.SDK_INT < 21) { - // Copy {@code buffer} into {@code temporaryBuffer}. - int bytesRemaining = buffer.remaining(); - if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) { - temporaryBuffer = new byte[bytesRemaining]; - } - int originalPosition = buffer.position(); - buffer.get(temporaryBuffer, 0, bytesRemaining); - buffer.position(originalPosition); - temporaryBufferOffset = 0; + + if (passthrough) { + submittedEncodedFrames += framesPerEncodedSample; + } else { + submittedPcmBytes += buffer.remaining(); } + + inputBuffer = buffer; } - buffer = useResampledBuffer ? resampledBuffer : buffer; + if (passthrough) { + // Passthrough buffers are not processed. + writeBuffer(inputBuffer, presentationTimeUs); + } else { + processBuffers(presentationTimeUs); + } + + if (!inputBuffer.hasRemaining()) { + inputBuffer = null; + return true; + } + return false; + } + + private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { + int count = audioProcessors.length; + int index = count; + while (index >= 0) { + ByteBuffer input = index > 0 ? outputBuffers[index - 1] + : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER); + if (index == count) { + writeBuffer(input, avSyncPresentationTimeUs); + } else { + AudioProcessor audioProcessor = audioProcessors[index]; + audioProcessor.queueInput(input); + ByteBuffer output = audioProcessor.getOutput(); + outputBuffers[index] = output; + if (output.hasRemaining()) { + // Handle the output as input to the next audio processor or the AudioTrack. + index++; + continue; + } + } + + if (input.hasRemaining()) { + // The input wasn't consumed and no output was produced, so give up for now. + return; + } + + // Get more input from upstream. + index--; + } + } + + @SuppressWarnings("ReferenceEquality") + private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) + throws WriteException { + if (!buffer.hasRemaining()) { + return true; + } + if (outputBuffer != null) { + Assertions.checkArgument(outputBuffer == buffer); + } else { + outputBuffer = buffer; + if (Util.SDK_INT < 21) { + int bytesRemaining = buffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; + } + int originalPosition = buffer.position(); + buffer.get(preV21OutputBuffer, 0, bytesRemaining); + buffer.position(originalPosition); + preV21OutputBufferOffset = 0; + } + } int bytesRemaining = buffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. int bytesPending = - (int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize)); + (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); int bytesToWrite = bufferSize - bytesPending; if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - if (bytesWritten >= 0) { - temporaryBufferOffset += bytesWritten; + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + buffer.position(buffer.position() + bytesWritten); } - buffer.position(buffer.position() + bytesWritten); } + } else if (tunneling) { + Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, + avSyncPresentationTimeUs); } else { - bytesWritten = tunneling - ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) - : writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (bytesWritten < 0) { throw new WriteException(bytesWritten); } if (!passthrough) { - submittedPcmBytes += bytesWritten; + writtenPcmBytes += bytesWritten; } if (bytesWritten == bytesRemaining) { if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; + writtenEncodedFrames += framesPerEncodedSample; } - currentSourceBuffer = null; + outputBuffer = null; return true; } return false; } /** - * Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, long)} is played in full. + * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended. + * + * @throws WriteException If an error occurs draining data to the track. */ - public void handleEndOfStream() { - if (isInitialized()) { - audioTrackUtil.handleEndOfStream(getSubmittedFrames()); - bytesUntilNextAvSync = 0; + public void playToEndOfStream() throws WriteException { + if (handledEndOfStream || !isInitialized()) { + return; } + + // Drain the audio processors. + boolean audioProcessorNeedsEndOfStream = false; + if (drainingAudioProcessorIndex == C.INDEX_UNSET) { + drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; + audioProcessorNeedsEndOfStream = true; + } + while (drainingAudioProcessorIndex < audioProcessors.length) { + AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex]; + if (audioProcessorNeedsEndOfStream) { + audioProcessor.queueEndOfStream(); + } + processBuffers(C.TIME_UNSET); + if (!audioProcessor.isEnded()) { + return; + } + audioProcessorNeedsEndOfStream = true; + drainingAudioProcessorIndex++; + } + + // Finish writing any remaining output to the track. + if (outputBuffer != null) { + writeBuffer(outputBuffer, C.TIME_UNSET); + if (outputBuffer != null) { + return; + } + } + + // Drain the track. + audioTrackUtil.handleEndOfStream(getWrittenFrames()); + bytesUntilNextAvSync = 0; + handledEndOfStream = true; + } + + /** + * Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been + * completely processed and played. + */ + public boolean isEnded() { + return !isInitialized() || (handledEndOfStream && !hasPendingData()); } /** @@ -762,7 +935,7 @@ public final class AudioTrack { */ public boolean hasPendingData() { return isInitialized() - && (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition() + && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() || overrideHasPendingData()); } @@ -815,9 +988,14 @@ public final class AudioTrack { /** * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. + *

+ * If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that + * audio processors do not output buffers with a different duration than their input, and buffer + * processors must produce output corresponding to their last input immediately after that input + * is queued. * * @param tunnelingAudioSessionId The audio session id to use. - * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. */ public void enableTunnelingV21(int tunnelingAudioSessionId) { Assertions.checkState(Util.SDK_INT >= 21); @@ -884,8 +1062,18 @@ public final class AudioTrack { if (isInitialized()) { submittedPcmBytes = 0; submittedEncodedFrames = 0; + writtenPcmBytes = 0; + writtenEncodedFrames = 0; framesPerEncodedSample = 0; - currentSourceBuffer = null; + inputBuffer = null; + outputBuffer = null; + for (int i = 0; i < audioProcessors.length; i++) { + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } + handledEndOfStream = false; + drainingAudioProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; @@ -920,6 +1108,9 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + audioProcessor.release(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } @@ -1063,10 +1254,6 @@ public final class AudioTrack { return audioTrack != null; } - private long pcmBytesToFrames(long byteCount) { - return byteCount / pcmFrameSize; - } - private long framesToDurationUs(long frameCount) { return (frameCount * C.MICROS_PER_SECOND) / sampleRate; } @@ -1076,7 +1263,11 @@ public final class AudioTrack { } private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes); + return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); + } + + private long getWrittenFrames() { + return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); } private void resetSyncParams() { @@ -1094,7 +1285,7 @@ public final class AudioTrack { */ private boolean needsPassthroughWorkarounds() { return Util.SDK_INT < 23 - && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); } /** @@ -1129,82 +1320,6 @@ public final class AudioTrack { sessionId); } - /** - * Converts the provided buffer into 16-bit PCM. - * - * @param buffer The buffer containing the data to convert. - * @param sourceEncoding The data encoding. - * @param out A buffer into which the output should be written, if its capacity is sufficient. - * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the - * capacity was insufficient for the output. - */ - private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding, - ByteBuffer out) { - int offset = buffer.position(); - int limit = buffer.limit(); - int size = limit - offset; - - int resampledSize; - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - resampledSize = size * 2; - break; - case C.ENCODING_PCM_24BIT: - resampledSize = (size / 3) * 2; - break; - case C.ENCODING_PCM_32BIT: - resampledSize = size / 2; - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - ByteBuffer resampledBuffer = out; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); - } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); - - // Samples are little endian. - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); - } - break; - case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(buffer.get(i + 1)); - resampledBuffer.put(buffer.get(i + 2)); - } - break; - case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(buffer.get(i + 2)); - resampledBuffer.put(buffer.get(i + 3)); - } - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - resampledBuffer.position(0); - return resampledBuffer; - } - @C.Encoding private static int getEncodingForMimeType(String mimeType) { switch (mimeType) { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java new file mode 100644 index 0000000000..e81d7e218a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2017 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.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.Encoding; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * An {@link AudioProcessor} that applies a mapping from input channels onto specified output + * channels. This can be used to reorder, duplicate or discard channels. + */ +/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor { + + private int channelCount; + private int sampleRateHz; + private int[] pendingOutputChannels; + + private boolean active; + private int[] outputChannels; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new processor that applies a channel mapping. + */ + public ChannelMappingAudioProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + /** + * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} + * to start using the new channel map. + * + * @see AudioTrack#configure(String, int, int, int, int, int[]) + */ + public void setChannelMap(int[] outputChannels) { + pendingOutputChannels = outputChannels; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) + throws UnhandledFormatException { + boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); + outputChannels = pendingOutputChannels; + if (outputChannels == null) { + active = false; + return outputChannelsChanged; + } + if (encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz + && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + + active = channelCount != outputChannels.length; + for (int i = 0; i < outputChannels.length; i++) { + int channelIndex = outputChannels[i]; + if (channelIndex >= channelCount) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + active |= (channelIndex != i); + } + return true; + } + + @Override + public boolean isActive() { + return active; + } + + @Override + public int getOutputChannelCount() { + return outputChannels == null ? channelCount : outputChannels.length; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int frameCount = (limit - position) / (2 * channelCount); + int outputSize = frameCount * outputChannels.length * 2; + if (buffer.capacity() < outputSize) { + buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + while (position < limit) { + for (int channelIndex : outputChannels) { + buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); + } + position += channelCount * 2; + } + inputBuffer.position(limit); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void release() { + flush(); + buffer = EMPTY_BUFFER; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8501c3858..e34068861d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private final AudioTrack audioTrack; private boolean passthroughEnabled; + private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; private int pcmEncoding; + private int channelCount; private long currentPositionUs; private boolean allowPositionDiscontinuity; @@ -121,13 +123,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before + * output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -185,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { + codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. passthroughMediaFormat = format.getFrameworkMediaFormatV16(); @@ -216,17 +222,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // output 16-bit PCM. pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; + channelCount = newFormat.channelCount; } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + int[] channelMap; + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { + channelMap = new int[this.channelCount]; + for (int i = 0; i < this.channelCount; i++) { + channelMap[i] = i; + } + } else { + channelMap = null; + } + + try { + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap); + } catch (AudioTrack.ConfigurationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } /** @@ -304,7 +326,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public boolean isEnded() { - return super.isEnded() && !audioTrack.hasPendingData(); + return super.isEnded() && audioTrack.isEnded(); } @Override @@ -353,8 +375,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onOutputStreamEnded() { - audioTrack.handleEndOfStream(); + protected void renderToEndOfStream() throws ExoPlaybackException { + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } @Override @@ -376,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the decoder is known to output six audio channels when provided with input with + * fewer than six channels. + *

+ * See [Internal: b/35655036]. + */ + private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) { + // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7. + return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte") + || Util.DEVICE.startsWith("heroqlte")); + } + private final class AudioTrackListener implements AudioTrack.Listener { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java new file mode 100644 index 0000000000..752f55a0ca --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2017 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.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ +/* package */ final class ResamplingAudioProcessor implements AudioProcessor { + + private int sampleRateHz; + private int channelCount; + @C.PcmEncoding + private int encoding; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ + public ResamplingAudioProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + encoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.encoding == encoding) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.encoding = encoding; + if (encoding == C.ENCODING_PCM_16BIT) { + buffer = EMPTY_BUFFER; + } + return true; + } + + @Override + public boolean isActive() { + return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + // Prepare the output buffer. + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int size = limit - position; + int resampledSize; + switch (encoding) { + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size / 2; + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + throw new IllegalStateException(); + } + if (buffer.capacity() < resampledSize) { + buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + + // Resample the little endian input and update the input/output buffers. + switch (encoding) { + case C.ENCODING_PCM_8BIT: + // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + for (int i = position; i < limit; i++) { + buffer.put((byte) 0); + buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); + } + break; + case C.ENCODING_PCM_24BIT: + // 24->16 bit resampling. Drop the least significant byte. + for (int i = position; i < limit; i += 3) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i + 2)); + } + break; + case C.ENCODING_PCM_32BIT: + // 32->16 bit resampling. Drop the two least significant bytes. + for (int i = position; i < limit; i += 4) { + buffer.put(inputBuffer.get(i + 2)); + buffer.put(inputBuffer.get(i + 3)); + } + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void release() { + flush(); + buffer = EMPTY_BUFFER; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d23ee769dd..5594d9a90e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; @@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; - private final EventDispatcher eventDispatcher; private final AudioTrack audioTrack; - private final DrmSessionManager drmSessionManager; private final FormatHolder formatHolder; + private final DecoderInputBuffer flagsOnlyBuffer; private DecoderCounters decoderCounters; private Format inputFormat; @@ -83,8 +84,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private DrmSession drmSession; private DrmSession pendingDrmSession; - @ReinitializationState - private int decoderReinitializationState; + @ReinitializationState private int decoderReinitializationState; private boolean decoderReceivedBuffers; private boolean audioTrackNeedsConfigure; @@ -102,10 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null); + AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { + this(eventHandler, eventListener, null, null, false, audioProcessors); } /** @@ -133,16 +134,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO); - eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); this.drmSessionManager = drmSessionManager; - formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + eventDispatcher = new EventDispatcher(eventHandler, eventListener); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); + formatHolder = new FormatHolder(); + flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @@ -174,13 +178,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } return; } // Try and read a format if we don't have one already. - if (inputFormat == null && !readFormat()) { - // We can't make progress without one. - return; + if (inputFormat == null) { + // We don't have a format yet, so try and read one. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } } // If we don't have a decoder yet, we need to instantiate one. @@ -193,8 +215,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioTrack.InitializationException | AudioTrack.WriteException - | AudioDecoderException e) { + } catch (AudioDecoderException | AudioTrack.ConfigurationException + | AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); @@ -255,7 +277,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.InitializationException, AudioTrack.WriteException { + AudioTrack.ConfigurationException, AudioTrack.InitializationException, + AudioTrack.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { @@ -274,8 +297,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } else { outputBuffer.release(); outputBuffer = null; - outputStreamEnded = true; - audioTrack.handleEndOfStream(); + processEndOfStream(); } return false; } @@ -324,7 +346,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements // We've already read an encrypted sample into buffer, and are waiting for keys. result = C.RESULT_BUFFER_READ; } else { - result = readSource(formatHolder, inputBuffer); + result = readSource(formatHolder, inputBuffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -365,6 +387,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements && (bufferEncrypted || !playClearSamplesWithoutKeys); } + private void processEndOfStream() throws ExoPlaybackException { + outputStreamEnded = true; + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + } + private void flushDecoder() throws ExoPlaybackException { waitingForKeys = false; if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -383,7 +414,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public boolean isEnded() { - return outputStreamEnded && !audioTrack.hasPendingData(); + return outputStreamEnded && audioTrack.isEnded(); } @Override @@ -513,15 +544,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements decoderReceivedBuffers = false; } - private boolean readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; - } - return false; - } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = inputFormat; inputFormat = newFormat; diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index b76f3e8d0c..84c89de427 100644 --- a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer { */ public long timeUs; - @BufferReplacementMode - private final int bufferReplacementMode; + @BufferReplacementMode private final int bufferReplacementMode; /** * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 1cd8d8464d..6fc149ba32 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -103,6 +104,7 @@ public class DefaultDrmSessionManager implements DrmSe public static final int MODE_RELEASE = 3; private static final String TAG = "OfflineDrmSessionMngr"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; @@ -280,6 +282,7 @@ public class DefaultDrmSessionManager implements DrmSe * required. * *

{@code mode} must be one of these: + *

    *
  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is * requested otherwise the offline license is restored. *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license @@ -288,6 +291,7 @@ public class DefaultDrmSessionManager implements DrmSe * requested otherwise the offline license is renewed. *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license * is released. + *
* * @param mode The mode to be set. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. @@ -337,6 +341,12 @@ public class DefaultDrmSessionManager implements DrmSe schemeInitData = psshData; } } + if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) + || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + schemeMimeType = CENC_SCHEME_MIME_TYPE; + } } state = STATE_OPENING; openInternal(true); diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 4d64187a8b..538db9e1d9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -31,7 +31,7 @@ public interface DrmSession { /** Wraps the exception which is the cause of the error state. */ class DrmSessionException extends Exception { - DrmSessionException(Exception e) { + public DrmSessionException(Exception e) { super(e); } @@ -70,8 +70,7 @@ public interface DrmSession { * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. */ - @State - int getState(); + @State int getState(); /** * Returns a {@link ExoMediaCrypto} for the open session. diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index e0c9ca5296..f9d5efffb1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } /** + * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request + * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @param keyRequestProperties Request properties to set when making key requests, or null. */ + @Deprecated public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; + this.keyRequestProperties = new HashMap<>(); + if (keyRequestProperties != null) { + this.keyRequestProperties.putAll(keyRequestProperties); + } + } + + /** + * Sets a header for key requests made by the callback. + * + * @param name The name of the header field. + * @param value The value of the field. + */ + public void setKeyRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (keyRequestProperties) { + keyRequestProperties.put(name, value); + } + } + + /** + * Clears a header for key requests made by the callback. + * + * @param name The name of the header field. + */ + public void clearKeyRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (keyRequestProperties) { + keyRequestProperties.remove(name); + } + } + + /** + * Clears all headers for key requests made by the callback. + */ + public void clearAllKeyRequestProperties() { + synchronized (keyRequestProperties) { + keyRequestProperties.clear(); + } } @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(url, new byte[0], null); + return executePost(dataSourceFactory, url, new byte[0], null); } @Override @@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { if (C.PLAYREADY_UUID.equals(uuid)) { requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); } - if (keyRequestProperties != null) { + synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } - return executePost(url, request.getData(), requestProperties); + return executePost(dataSourceFactory, url, request.getData(), requestProperties); } - private byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { + private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url, + byte[] data, Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index a11d65d4d3..ad44574af9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.media.MediaDrm; -import android.net.Uri; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; @@ -27,24 +26,14 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; -import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Period; -import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSourceInputStream; -import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.HashMap; @@ -58,28 +47,6 @@ public final class OfflineLicenseHelper { private final DefaultDrmSessionManager drmSessionManager; private final HandlerThread handlerThread; - /** - * Helper method to download a DASH manifest. - * - * @param dataSource The {@link HttpDataSource} from which the manifest should be read. - * @param manifestUriString The URI of the manifest to be read. - * @return An instance of {@link DashManifest}. - * @throws IOException If an error occurs reading data from the stream. - * @see DashManifestParser - */ - public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString) - throws IOException { - DataSourceInputStream inputStream = new DataSourceInputStream( - dataSource, new DataSpec(Uri.parse(manifestUriString))); - try { - inputStream.open(); - DashManifestParser parser = new DashManifestParser(); - return parser.parse(dataSource.getUri(), inputStream); - } finally { - inputStream.close(); - } - } - /** * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when * you're done with the helper instance. @@ -93,7 +60,7 @@ public final class OfflineLicenseHelper { public static OfflineLicenseHelper newWidevineInstance( String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null); + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); } /** @@ -174,7 +141,7 @@ public final class OfflineLicenseHelper { */ public byte[] download(HttpDataSource dataSource, String manifestUriString) throws IOException, InterruptedException, DrmSessionException { - return download(dataSource, downloadManifest(dataSource, manifestUriString)); + return download(dataSource, DashUtil.loadManifest(dataSource, manifestUriString)); } /** @@ -210,11 +177,7 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation); - if (initializationChunk == null) { - return null; - } - Format sampleFormat = initializationChunk.getSampleFormat(); + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); if (sampleFormat != null) { drmInitData = sampleFormat.drmInitData; } @@ -288,28 +251,4 @@ public final class OfflineLicenseHelper { return session; } - private static InitializationChunk loadInitializationChunk(final DataSource dataSource, - final Representation representation) throws IOException, InterruptedException { - RangedUri rangedUri = representation.getInitializationUri(); - if (rangedUri == null) { - return null; - } - DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start, - rangedUri.length, representation.getCacheKey()); - InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, - representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, - newWrappedExtractor(representation.format)); - initializationChunk.load(); - return initializationChunk; - } - - private static ChunkExtractorWrapper newWrappedExtractor(final Format format) { - final String mimeType = format.containerMimeType; - final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) - || mimeType.startsWith(MimeTypes.AUDIO_WEBM); - final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */, - false /* resendFormatOnInit */); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java b/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java index 505750efaa..f0e748d722 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java @@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception { /** * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. */ - @Reason - public final int reason; + @Reason public final int reason; /** * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index bca5ecf3bd..87355a6c78 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; @@ -27,6 +28,8 @@ import java.util.Arrays; */ public final class DefaultExtractorInput implements ExtractorInput { + private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; + private static final int PEEK_MAX_FREE_SPACE = 512 * 1024; private static final byte[] SCRATCH_SPACE = new byte[4096]; private final DataSource dataSource; @@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput { this.dataSource = dataSource; this.position = position; this.streamLength = length; - peekBuffer = new byte[8 * 1024]; + peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; } @Override @@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput { private void ensureSpaceForPeek(int length) { int requiredLength = peekBufferPosition + length; if (requiredLength > peekBuffer.length) { - peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength)); + int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2, + requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE); + peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity); } } @@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput { private void updatePeekBuffer(int bytesConsumed) { peekBufferLength -= bytesConsumed; peekBufferPosition = 0; - System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); + byte[] newPeekBuffer = peekBuffer; + if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) { + newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + } + System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength); + peekBuffer = newPeekBuffer; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index b3bcd97048..8aff8858a1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput { private Format downstreamFormat; // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private boolean pendingFormatAdjustment; + private Format lastUnadjustedFormat; private long sampleOffsetUs; private long totalBytesWritten; private Allocation lastAllocation; @@ -265,40 +267,42 @@ public final class DefaultTrackOutput implements TrackOutput { * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the - * caller requires that the format of the stream be read even if it's not changing. + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * be set if the buffer's timestamp is less than this value. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished, - long decodeOnlyUntilUs) { - switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) { - case C.RESULT_NOTHING_READ: - if (loadingFinished) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } - return C.RESULT_NOTHING_READ; + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean loadingFinished, long decodeOnlyUntilUs) { + int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + downstreamFormat, extrasHolder); + switch (result) { case C.RESULT_FORMAT_READ: downstreamFormat = formatHolder.format; return C.RESULT_FORMAT_READ; case C.RESULT_BUFFER_READ: - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + if (!buffer.isEndOfStream()) { + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Write the sample data into the holder. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + // Advance the read head. + dropDownstreamTo(extrasHolder.nextOffset); } - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); - } - // Write the sample data into the holder. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); - // Advance the read head. - dropDownstreamTo(extrasHolder.nextOffset); return C.RESULT_BUFFER_READ; + case C.RESULT_NOTHING_READ: + return C.RESULT_NOTHING_READ; default: throw new IllegalStateException(); } @@ -445,23 +449,24 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of - * samples subsequently queued to the buffer. The offset is also used to adjust - * {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently - * passed to {@link #format(Format)}. + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently queued to the buffer. * - * @param format The format. * @param sampleOffsetUs The timestamp offset in microseconds. */ - public void formatWithOffset(Format format, long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - format(format); + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + pendingFormatAdjustment = true; + } } @Override public void format(Format format) { Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); boolean formatChanged = infoQueue.format(adjustedFormat); + lastUnadjustedFormat = format; + pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); } @@ -518,6 +523,9 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, byte[] encryptionKey) { + if (pendingFormatAdjustment) { + format(lastUnadjustedFormat); + } if (!startWriteOperation()) { infoQueue.commitSampleTimestamp(timeUs); return; @@ -754,23 +762,34 @@ public final class DefaultTrackOutput implements TrackOutput { * and the absolute position of the first byte that may still be required after the current * sample has been read. May be null if the caller requires that the format of the stream be * read even if it's not changing. + * @param formatRequired Whether the caller requires that the format of the stream be read even + * if it's not changing. A sample will never be read if set to true, however it is still + * possible for the end of stream or nothing to be read. + * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param downstreamFormat The current downstream {@link Format}. If the format of the next * sample is different to the current downstream format then a format will be read. * @param extrasHolder The holder into which extra sample information should be written. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} * or {@link C#RESULT_BUFFER_READ}. */ + @SuppressWarnings("ReferenceEquality") public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - Format downstreamFormat, BufferExtrasHolder extrasHolder) { + boolean formatRequired, boolean loadingFinished, Format downstreamFormat, + BufferExtrasHolder extrasHolder) { if (queueSize == 0) { - if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) { + if (loadingFinished) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null + && (formatRequired || upstreamFormat != downstreamFormat)) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; } - return C.RESULT_NOTHING_READ; } - if (buffer == null || formats[relativeReadIndex] != downstreamFormat) { + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 38b0325cba..de3dfd5266 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -102,4 +102,5 @@ public interface Extractor { * Releases all kept resources. */ void release(); + } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index a547f745ca..a59cb1d1f2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -23,17 +23,18 @@ public interface ExtractorOutput { /** * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

- * The same {@link TrackOutput} is returned if multiple calls are made with the same - * {@code trackId}. + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param trackId A track identifier. + * @param id A track identifier. + * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ - TrackOutput track(int trackId); + TrackOutput track(int id, int type); /** * Called when all tracks have been identified, meaning no new {@code trackId} values will be - * passed to {@link #track(int)}. + * passed to {@link #track(int, int)}. */ void endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 7e2a1b4a23..75d8b4cf2d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +27,18 @@ import java.util.regex.Pattern; */ public final class GaplessInfoHolder { + /** + * A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed + * to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback + * information are decoded. + */ + public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() { + @Override + public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { + return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2); + } + }; + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 5b396749ac..218e6ffd82 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap { boolean hasAudio = (flags & 0x04) != 0; boolean hasVideo = (flags & 0x01) != 0; if (hasAudio && audioReader == null) { - audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); + audioReader = new AudioTagPayloadReader( + extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO)); } if (hasVideo && videoReader == null) { - videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); + videoReader = new VideoTagPayloadReader( + extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } if (metadataReader == null) { metadataReader = new ScriptTagPayloadReader(null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 970335e9d2..51ce819282 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor { case 3: currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; break; + case 15: + currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } @@ -1462,6 +1465,7 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("Unrecognized codec identifier."); } + int type; Format format; @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; @@ -1469,10 +1473,12 @@ public final class MatroskaExtractor implements Extractor { // TODO: Consider reading the name elements of the tracks and, if present, incorporating them // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { + type = C.TRACK_TYPE_AUDIO; format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, initializationData, drmInitData, selectionFlags, language); } else if (MimeTypes.isVideo(mimeType)) { + type = C.TRACK_TYPE_VIDEO; if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; @@ -1485,17 +1491,19 @@ public final class MatroskaExtractor implements Extractor { Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); } else { throw new ParserException("Unexpected MIME type."); } - this.output = output.track(number); + this.output = output.track(number, type); this.output.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 9bdefeceaf..b0faad71c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts data from an MP3 file. @@ -51,6 +54,23 @@ public final class Mp3Extractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 2; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; @@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor { * Constructs a new {@link Mp3Extractor}. */ public Mp3Extractor() { - this(C.TIME_UNSET); + this(0); } /** * Constructs a new {@link Mp3Extractor}. * + * @param flags Flags that control the extractor's behavior. + */ + public Mp3Extractor(@Flags int flags) { + this(flags, C.TIME_UNSET); + } + + /** + * Constructs a new {@link Mp3Extractor}. + * + * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ - public Mp3Extractor(long forcedFirstSampleTimestampUs) { + public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { + this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); @@ -118,7 +150,7 @@ public final class Mp3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); } @@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor { trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); } return readSample(input); } @@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor { byte[] id3Data = new byte[tagLength]; System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); - metadata = new Id3Decoder().decode(id3Data, tagLength); + // We need to parse enough ID3 metadata to retrieve any gapless playback information even + // if ID3 metadata parsing is disabled. + Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 + ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } @@ -350,7 +387,8 @@ public final class Mp3Extractor implements Extractor { } } - if (seeker == null) { + if (seeker == null || (!seeker.isSeekable() + && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { // Repopulate the synchronized header in case we had to skip an invalid seeking header, which // would give an invalid CBR bitrate. input.resetPeekPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 5288a3e6ba..54141f2545 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -332,6 +332,9 @@ import java.util.List; return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } + // Omit any sample at the end point of an edit for audio tracks. + boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO; + // Count the number of samples after applying edits. int editedSampleCount = 0; int nextSampleIndex = 0; @@ -342,7 +345,8 @@ import java.util.List; long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); + int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, + false); editedSampleCount += endIndex - startIndex; copyMetadata |= nextSampleIndex != startIndex; nextSampleIndex = endIndex; @@ -365,7 +369,7 @@ import java.util.List; long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); + int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); if (copyMetadata) { int count = endIndex - startIndex; System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); @@ -716,6 +720,9 @@ import java.util.List; case 2: stereoMode = C.STEREO_MODE_LEFT_RIGHT; break; + case 3: + stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index f7cc42c48f..a228a9b775 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -106,7 +106,6 @@ public final class FragmentedMp4Extractor implements Extractor { private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); - private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information 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}; @@ -118,8 +117,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int STATE_READING_SAMPLE_CONTINUE = 4; // Workarounds. - @Flags - private final int flags; + @Flags private final int flags; private final Track sideloadedTrack; // Track-linked data bundle, accessible as a whole through trackID. @@ -127,8 +125,8 @@ public final class FragmentedMp4Extractor implements Extractor { // Temporary arrays. private final ParsableByteArray nalStartCode; - private final ParsableByteArray nalLength; - private final ParsableByteArray nalPayload; + private final ParsableByteArray nalPrefix; + private final ParsableByteArray nalBuffer; private final ParsableByteArray encryptionSignalByte; // Adjusts sample timestamps. @@ -154,17 +152,25 @@ public final class FragmentedMp4Extractor implements Extractor { private int sampleSize; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; + private boolean processSeiNalUnitPayload; // Extractor output. private ExtractorOutput extractorOutput; private TrackOutput eventMessageTrackOutput; - private TrackOutput cea608TrackOutput; + private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; public FragmentedMp4Extractor() { - this(0, null); + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, null); } /** @@ -172,24 +178,24 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, null, timestampAdjuster); + this(flags, timestampAdjuster, null); } /** * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor * will not receive a moov box in the input data. - * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack, - TimestampAdjuster timestampAdjuster) { - this.sideloadedTrack = sideloadedTrack; + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); - nalLength = new ParsableByteArray(4); - nalPayload = new ParsableByteArray(1); + nalPrefix = new ParsableByteArray(5); + nalBuffer = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); @@ -209,7 +215,7 @@ public final class FragmentedMp4Extractor implements Extractor { public void init(ExtractorOutput output) { extractorOutput = output; if (sideloadedTrack != null) { - TrackBundle bundle = new TrackBundle(output.track(0)); + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); maybeInitExtraTracks(); @@ -420,7 +426,7 @@ public final class FragmentedMp4Extractor implements Extractor { // We need to create the track bundles. for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i)); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); trackBundle.init(track, defaultSampleValuesArray.get(track.id)); trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); @@ -449,14 +455,16 @@ public final class FragmentedMp4Extractor implements Extractor { private void maybeInitExtraTracks() { if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { - eventMessageTrackOutput = extractorOutput.track(trackBundles.size()); + eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { - cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1); + if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { + TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, + C.TRACK_TYPE_TEXT); cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; } } @@ -1059,49 +1067,49 @@ public final class FragmentedMp4Extractor implements Extractor { if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. - byte[] nalLengthData = nalLength.data; - nalLengthData[0] = 0; - nalLengthData[1] = 0; - nalLengthData[2] = 0; - int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; // NAL units are length delimited, but the decoder requires start code delimited units. // Loop until we've written the sample to the track output, replacing length delimiters with // start codes as we encounter them. while (sampleBytesWritten < sampleSize) { if (sampleCurrentNalBytesRemaining == 0) { - // Read the NAL length so that we know where we find the next one. - input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); - nalLength.setPosition(0); - sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); output.sampleData(nalStartCode, 4); - sampleBytesWritten += 4; + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + processSeiNalUnitPayload = cea608TrackOutputs != null + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); + sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; - if (cea608TrackOutput != null) { - byte[] nalPayloadData = nalPayload.data; - // Peek the NAL unit type byte. - input.peekFully(nalPayloadData, 0, 1); - if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { - // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte. - nalPayload.reset(sampleCurrentNalBytesRemaining); - input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining); - // Write the SEI unit straight to the output. - output.sampleData(nalPayload, sampleCurrentNalBytesRemaining); - sampleBytesWritten += sampleCurrentNalBytesRemaining; - sampleCurrentNalBytesRemaining = 0; - // Unescape and process the SEI unit. - int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit()); - nalPayload.setPosition(1); // Skip the NAL unit type byte. - nalPayload.setLimit(unescapedLength); - CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload, - cea608TrackOutput); - } - } } else { - // Write the payload of the NAL unit. - int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); + CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, + cea608TrackOutputs); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 3759a80fd4..d0e770abdc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -83,8 +83,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private final ParsableByteArray atomHeader; private final Stack containerAtoms; - @State - private int parserState; + @State private int parserState; private int atomType; private long atomSize; private int atomHeaderBytesRead; @@ -344,7 +343,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { continue; } - Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index c723704d37..f1c4e99ec1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -75,8 +75,7 @@ public final class Track { * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each * sample. */ - @Transformation - public final int sampleTransformation; + @Transformation public final int sampleTransformation; /** * Track encryption boxes for the different track sample descriptions. Entries may be null. @@ -94,7 +93,7 @@ public final class Track { public final long[] editListMediaTimes; /** - * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for + * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for * other track types. */ public final int nalUnitLengthFieldLength; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 5f41126737..cc3c5de311 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -75,7 +76,7 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); output.endTracks(); // TODO: fix the case if sniff() isn't called streamReader.init(output, trackOutput); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index f9957aebe5..7840eafce6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -65,7 +65,7 @@ public final class RawCcExtractor implements Extractor { @Override public void init(ExtractorOutput output) { output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_TEXT); output.endTracks(); trackOutput.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 52faa8c673..248161f28f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. */ -/* package */ final class Ac3Reader implements ElementaryStreamReader { +public final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String trackFormatId; private TrackOutput output; private int state; @@ -84,7 +85,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { - output = extractorOutput.track(generator.getNextId()); + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -180,8 +183,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; headerScratchBits.skipBits(40); isEac3 = headerScratchBits.readBits(5) == 16; headerScratchBits.setPosition(headerScratchBits.getPosition() - 45); - format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null) - : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null); + format = isEac3 + ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, trackFormatId, language , null) + : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, trackFormatId, language, null); output.format(format); } sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 47cb217fc7..7277df5bb8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -33,7 +33,7 @@ import java.util.Collections; /** * Parses a continuous ADTS byte stream and extracts individual frames. */ -/* package */ final class AdtsReader implements ElementaryStreamReader { +public final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; @@ -61,6 +61,7 @@ import java.util.Collections; private final ParsableByteArray id3HeaderBuffer; private final String language; + private String formatId; private TrackOutput output; private TrackOutput id3Output; @@ -108,11 +109,14 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); if (exposeId3) { - id3Output = extractorOutput.track(idGenerator.getNextId()); - id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { id3Output = new DummyTrackOutput(); } @@ -300,7 +304,7 @@ import java.util.Collections; Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig), null, 0, language); // In this class a sample is an access unit, but the MediaFormat sample rate specifies the diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 31aa88d11a..e8b664d5ab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,9 +17,15 @@ package com.google.android.exoplayer2.extractor.ts; import android.support.annotation.IntDef; import android.util.SparseArray; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Default implementation for {@link TsPayloadReader.Factory}. @@ -27,28 +33,47 @@ import java.lang.annotation.RetentionPolicy; public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { /** - * Flags controlling elementary stream readers behaviour. + * Flags controlling elementary stream readers' behavior. */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, - FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM}) + FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, + FLAG_OVERRIDE_CAPTION_DESCRIPTORS}) public @interface Flags { } public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; - public static final int FLAG_IGNORE_AAC_STREAM = 2; - public static final int FLAG_IGNORE_H264_STREAM = 4; - public static final int FLAG_DETECT_ACCESS_UNITS = 8; - public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 16; + public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; + public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; + public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; + public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; + public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; - @Flags - private final int flags; + private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; + + @Flags private final int flags; + private final List closedCaptionFormats; public DefaultTsPayloadReaderFactory() { - this(0); + this(0, Collections.emptyList()); } - public DefaultTsPayloadReaderFactory(@Flags int flags) { + /** + * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created + * readers. + * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with + * embedded closed captions when no caption service descriptors are provided. If + * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides + * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a + * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will + * be exposed. + */ + public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { this.flags = flags; + if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) { + closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null, + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + } + this.closedCaptionFormats = closedCaptionFormats; } @Override @@ -74,10 +99,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: - return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader( - new H264Reader(isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); + return isSet(FLAG_IGNORE_H264_STREAM) ? null + : new PesReader(new H264Reader(buildSeiReader(esInfo), + isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader()); + return new PesReader(new H265Reader(buildSeiReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? null : new SectionReader(new SpliceInfoSectionReader()); @@ -88,6 +114,55 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact } } + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor + * is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link SeiReader} for closed caption tracks. + */ + private SeiReader buildSeiReader(EsInfo esInfo) { + if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { + return new SeiReader(closedCaptionFormats); + } + ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); + List closedCaptionFormats = this.closedCaptionFormats; + while (scratchDescriptorData.bytesLeft() > 0) { + int descriptorTag = scratchDescriptorData.readUnsignedByte(); + int descriptorLength = scratchDescriptorData.readUnsignedByte(); + int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength; + if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) { + // Note: see ATSC A/65 for detailed information about the caption service descriptor. + closedCaptionFormats = new ArrayList<>(); + int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F; + for (int i = 0; i < numberOfServices; i++) { + String language = scratchDescriptorData.readString(3); + int captionTypeByte = scratchDescriptorData.readUnsignedByte(); + boolean isDigital = (captionTypeByte & 0x80) != 0; + String mimeType; + int accessibilityChannel; + if (isDigital) { + mimeType = MimeTypes.APPLICATION_CEA708; + accessibilityChannel = captionTypeByte & 0x3F; + } else { + mimeType = MimeTypes.APPLICATION_CEA608; + accessibilityChannel = 1; + } + closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null, + Format.NO_VALUE, 0, language, accessibilityChannel, null)); + // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14). + scratchDescriptorData.skipBytes(2); + } + } else { + // Unknown descriptor. Ignore. + } + scratchDescriptorData.setPosition(nextDescriptorPosition); + } + return new SeiReader(closedCaptionFormats); + } + private boolean isSet(@Flags int flag) { return (flags & flag) != 0; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 9707685295..df1e8816f0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous DTS byte stream and extracts individual samples. */ -/* package */ final class DtsReader implements ElementaryStreamReader { +public final class DtsReader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -79,7 +80,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -165,7 +168,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { - format = DtsUtil.parseDtsFormat(frameData, null, language, null); + format = DtsUtil.parseDtsFormat(frameData, formatId, language, null); output.format(format); } sampleSize = DtsUtil.getDtsFrameSize(frameData); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 02ea6d7c4e..7266f847c4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -30,13 +30,14 @@ import java.util.Collections; /** * Parses a continuous H262 byte stream and extracts individual frames. */ -/* package */ final class H262Reader implements ElementaryStreamReader { +public final class H262Reader implements ElementaryStreamReader { private static final int START_PICTURE = 0x00; private static final int START_SEQUENCE_HEADER = 0xB3; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private String formatId; private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. @@ -78,7 +79,9 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); } @Override @@ -126,7 +129,7 @@ import java.util.Collections; int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer); + Pair result = parseCsdBuffer(csdBuffer, formatId); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -166,10 +169,11 @@ import java.util.Collections; * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. + * @param formatId The id for the generated format. May be null. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or * 0 if the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; @@ -195,7 +199,7 @@ import java.util.Collections; break; } - Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_MPEG2, null, + Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index ed4682d9b9..8206ed7d6d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -33,12 +33,13 @@ import java.util.List; /** * Parses a continuous H264 byte stream and extracts individual frames. */ -/* package */ final class H264Reader implements ElementaryStreamReader { +public final class H264Reader implements ElementaryStreamReader { private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + private final SeiReader seiReader; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; private final NalUnitTargetBuffer sps; @@ -47,8 +48,8 @@ import java.util.List; private long totalBytesWritten; private final boolean[] prefixFlags; + private String formatId; private TrackOutput output; - private SeiReader seiReader; private SampleReader sampleReader; // State that should not be reset on seek. @@ -61,15 +62,17 @@ import java.util.List; private final ParsableByteArray seiWrapper; /** + * @param seiReader An SEI reader for consuming closed caption channels. * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * synchronization samples (key-frames). * @param detectAccessUnits Whether to split the input stream into access units (samples) based on * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). */ - public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) { - prefixFlags = new boolean[3]; + public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) { + this.seiReader = seiReader; this.allowNonIdrKeyframes = allowNonIdrKeyframes; this.detectAccessUnits = detectAccessUnits; + prefixFlags = new boolean[3]; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -88,9 +91,11 @@ import java.util.List; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + seiReader.createTracks(extractorOutput, idGenerator); } @Override @@ -175,7 +180,7 @@ import java.util.List; initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); hasOutputFormat = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index a78169a054..712ca8d69c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -30,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous H.265 byte stream and extracts individual frames. */ -/* package */ final class H265Reader implements ElementaryStreamReader { +public final class H265Reader implements ElementaryStreamReader { private static final String TAG = "H265Reader"; @@ -44,9 +44,11 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private final SeiReader seiReader; + + private String formatId; private TrackOutput output; private SampleReader sampleReader; - private SeiReader seiReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -66,7 +68,11 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - public H265Reader() { + /** + * @param seiReader An SEI reader for consuming closed caption channels. + */ + public H265Reader(SeiReader seiReader) { + this.seiReader = seiReader; prefixFlags = new boolean[3]; vps = new NalUnitTargetBuffer(VPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128); @@ -90,9 +96,11 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + seiReader.createTracks(extractorOutput, idGenerator); } @Override @@ -183,7 +191,7 @@ import java.util.Collections; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { - output.format(parseMediaFormat(vps, sps, pps)); + output.format(parseMediaFormat(formatId, vps, sps, pps)); hasOutputFormat = true; } } @@ -205,8 +213,8 @@ import java.util.Collections; } } - private static Format parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, - NalUnitTargetBuffer pps) { + private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -311,7 +319,7 @@ import java.util.Collections; } } - return Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, + return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index c19bc9d14e..98e1309143 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses ID3 data and extracts individual text information frames. */ -/* package */ final class Id3Reader implements ElementaryStreamReader { +public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; @@ -56,9 +56,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, - null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index c67e7ad0ab..82fb84b291 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. */ -/* package */ final class MpegAudioReader implements ElementaryStreamReader { +public final class MpegAudioReader implements ElementaryStreamReader { private static final int STATE_FINDING_HEADER = 0; private static final int STATE_READING_HEADER = 1; @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final MpegAudioHeader header; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -76,7 +77,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -176,9 +179,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; - Format format = Format.createAudioSampleFormat(null, header.mimeType, null, Format.NO_VALUE, - MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, null, null, 0, - language); + Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, + null, null, 0, language); output.format(format); hasOutputFormat = true; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 6e2e42d8e2..1e5d480ea1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,27 +15,51 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.text.cea.CeaUtil; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; /** * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ /* package */ final class SeiReader { - private final TrackOutput output; + private final List closedCaptionFormats; + private final TrackOutput[] outputs; - public SeiReader(TrackOutput output) { - this.output = output; - output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, - Format.NO_VALUE, 0, null, null)); + /** + * @param closedCaptionFormats A list of formats for the closed caption channels to expose. + */ + public SeiReader(List closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } + + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, + Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, + channelFormat.accessibilityChannel, null)); + outputs[i] = output; + } } public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { - CeaUtil.consume(pesTimeUs, seiBuffer, output); + CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 057fa636ce..27838d4c25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -36,9 +36,10 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 61d66afbc2..65b97c8a73 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; @@ -34,7 +35,12 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Facilitates the extraction of data from the MPEG-2 TS container format. @@ -53,6 +59,27 @@ public final class TsExtractor implements Extractor { }; + /** + * Modes for the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) + public @interface Mode {} + + /** + * Behave as defined in ISO/IEC 13818-1. + */ + public static final int MODE_NORMAL = 0; + /** + * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. + */ + public static final int MODE_SINGLE_PMT = 1; + /** + * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore + * continuity counters. + */ + public static final int MODE_HLS = 2; + public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC = 0x0F; @@ -78,8 +105,8 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; - private final boolean hlsMode; - private final TimestampAdjuster timestampAdjuster; + @Mode private final int mode; + private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; @@ -89,31 +116,30 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; public TsExtractor() { - this(new TimestampAdjuster(0)); - } - - /** - * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. - */ - public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false); + this(MODE_NORMAL, new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory()); } /** + * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. - * @param hlsMode Whether the extractor should be used in HLS mode. If true, {@link TrackOutput}s - * are mapped by their type (instead of PID) and continuity counters are ignored. */ - public TsExtractor(TimestampAdjuster timestampAdjuster, - TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) { - this.timestampAdjuster = timestampAdjuster; + public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster, + TsPayloadReader.Factory payloadReaderFactory) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); - this.hlsMode = hlsMode; + this.mode = mode; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { + timestampAdjusters = Collections.singletonList(timestampAdjuster); + } else { + timestampAdjusters = new ArrayList<>(); + timestampAdjusters.add(timestampAdjuster); + } tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); @@ -150,7 +176,10 @@ public final class TsExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - timestampAdjuster.reset(); + int timestampAdjustersCount = timestampAdjusters.size(); + for (int i = 0; i < timestampAdjustersCount; i++) { + timestampAdjusters.get(i).reset(); + } tsPacketBuffer.reset(); continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. @@ -215,7 +244,7 @@ public final class TsExtractor implements Extractor { // Discontinuity check. boolean discontinuityFound = false; int continuityCounter = tsScratch.readBits(4); - if (!hlsMode) { + if (mode != MODE_HLS) { int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter); if (previousCounter == continuityCounter) { @@ -307,8 +336,12 @@ public final class TsExtractor implements Extractor { } else { int pid = patScratch.readBits(13); tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); + remainingPmts++; } } + if (mode != MODE_HLS) { + tsPayloadReaders.remove(TS_PAT_PID); + } } } @@ -345,10 +378,22 @@ public final class TsExtractor implements Extractor { // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. return; } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16), - // reserved (2), version_number (5), current_next_indicator (1), // section_number (8), + // TimestampAdjuster assignment. + TimestampAdjuster timestampAdjuster; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) { + timestampAdjuster = timestampAdjusters.get(0); + } else { + timestampAdjuster = new TimestampAdjuster( + timestampAdjusters.get(0).getFirstSampleTimestampUs()); + timestampAdjusters.add(timestampAdjuster); + } + + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) + sectionData.skipBytes(2); + int programNumber = sectionData.readUnsignedShort(); + // reserved (2), version_number (5), current_next_indicator (1), section_number (8), // last_section_number (8), reserved (3), PCR_PID (13) - sectionData.skipBytes(9); + sectionData.skipBytes(5); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -358,13 +403,13 @@ public final class TsExtractor implements Extractor { // Skip the descriptors. sectionData.skipBytes(programInfoLength); - if (hlsMode && id3Reader == null) { + if (mode == MODE_HLS && id3Reader == null) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, - new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } int remainingEntriesLength = sectionData.bytesLeft(); @@ -381,19 +426,20 @@ public final class TsExtractor implements Extractor { } remainingEntriesLength -= esInfoLength + 5; - int trackId = hlsMode ? streamType : elementaryPid; + int trackId = mode == MODE_HLS ? streamType : elementaryPid; if (trackIds.get(trackId)) { continue; } trackIds.put(trackId, true); TsPayloadReader reader; - if (hlsMode && streamType == TS_STREAM_TYPE_ID3) { + if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) { reader = id3Reader; } else { reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); if (reader != null) { - reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } } @@ -401,16 +447,20 @@ public final class TsExtractor implements Extractor { tsPayloadReaders.put(elementaryPid, reader); } } - if (hlsMode) { + if (mode == MODE_HLS) { if (!tracksEnded) { output.endTracks(); + remainingPmts = 0; + tracksEnded = true; } } else { - tsPayloadReaders.remove(TS_PAT_PID); tsPayloadReaders.remove(pid); - output.endTracks(); + remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1; + if (remainingPmts == 0) { + output.endTracks(); + tracksEnded = true; + } } - tracksEnded = true; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 5785c50a7b..4169e0f3a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -81,17 +81,63 @@ public interface TsPayloadReader { */ final class TrackIdGenerator { - private final int firstId; - private final int idIncrement; - private int generatedIdCount; + private static final int ID_UNSET = Integer.MIN_VALUE; - public TrackIdGenerator(int firstId, int idIncrement) { - this.firstId = firstId; - this.idIncrement = idIncrement; + private final String formatIdPrefix; + private final int firstTrackId; + private final int trackIdIncrement; + private int trackId; + private String formatId; + + public TrackIdGenerator(int firstTrackId, int trackIdIncrement) { + this(ID_UNSET, firstTrackId, trackIdIncrement); } - public int getNextId() { - return firstId + idIncrement * generatedIdCount++; + public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) { + this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : ""; + this.firstTrackId = firstTrackId; + this.trackIdIncrement = trackIdIncrement; + trackId = ID_UNSET; + } + + /** + * Generates a new set of track and track format ids. Must be called before {@code get*} + * methods. + */ + public void generateNewId() { + trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement; + formatId = formatIdPrefix + trackId; + } + + /** + * Returns the last generated track id. Must be called after the first {@link #generateNewId()} + * call. + * + * @return The last generated track id. + */ + public int getTrackId() { + maybeThrowUninitializedError(); + return trackId; + } + + /** + * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be + * called after the first {@link #generateNewId()} call. + * + * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as + * format id. + */ + public String getFormatId() { + maybeThrowUninitializedError(); + return formatId; + } + + private void maybeThrowUninitializedError() { + if (trackId == ID_UNSET) { + throw new IllegalStateException("generateNewId() must be called before retrieving ids."); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 3d9f8166ab..cb46aa5519 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -60,7 +60,7 @@ public final class WavExtractor implements Extractor, SeekMap { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); wavHeader = null; output.endTracks(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 9be1c59baf..3fbbfac652 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -183,6 +183,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; @@ -342,6 +343,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); @@ -478,10 +480,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { + renderToEndOfStream(); return; } if (format == null) { - readFormat(); + // We don't have a format yet, so try and read one. + buffer.clear(); + int result = readSource(formatHolder, buffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(buffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } } maybeInitCodec(); if (codec != null) { @@ -495,13 +512,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { decoderCounters.ensureUpdated(); } - private void readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - } - } - protected void flushCodec() throws ExoPlaybackException { codecHotswapDeadlineMs = C.TIME_UNSET; inputIndex = C.INDEX_UNSET; @@ -513,7 +523,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { - // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053]. releaseCodec(); maybeInitCodec(); } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -592,7 +601,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; } adaptiveReconfigurationBytes = buffer.data.position(); - result = readSource(formatHolder, buffer); + result = readSource(formatHolder, buffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -779,18 +788,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codec The {@link MediaCodec} instance. * @param outputFormat The new output format. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { - // Do nothing. - } - - /** - * Called when the output stream ends, meaning that the last output buffer has been processed and - * the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag has been propagated through the decoder. - *

- * The default implementation is a no-op. - */ - protected void onOutputStreamEnded() { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { // Do nothing. } @@ -865,7 +866,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputIndex < 0) { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } if (outputIndex >= 0) { // We've dequeued a buffer. if (shouldSkipAdaptationWorkaroundOutputBuffer) { @@ -904,9 +920,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer)) { + boolean processedOutputBuffer; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } + + if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); outputIndex = C.INDEX_UNSET; return true; @@ -918,7 +952,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes a new output format. */ - private void processOutputFormat() { + private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); if (codecNeedsAdaptationWorkaround && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT @@ -974,6 +1008,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException; + /** + * Incrementally renders any remaining output. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output. + */ + protected void renderToEndOfStream() throws ExoPlaybackException { + // Do nothing. + } + /** * Processes an end of stream signal. * @@ -986,7 +1031,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { maybeInitCodec(); } else { outputStreamEnded = true; - onOutputStreamEnded(); + renderToEndOfStream(); } } @@ -1008,6 +1053,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

* If true is returned, the renderer will work around the issue by releasing the decoder and * instantiating a new one rather than flushing the current instance. + *

+ * See [Internal: b/8347958, b/8543366]. * * @param name The name of the decoder. * @return True if the decoder is known to fail when flushed. @@ -1077,6 +1124,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

* If true is returned, the renderer will work around the issue by instantiating a new decoder * when this case occurs. + *

+ * See [Internal: b/8578467, b/23361053]. * * @param name The name of the decoder. * @return True if the decoder is known to behave incorrectly if flushed after receiving an input @@ -1089,6 +1138,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); } + /** + * Returns whether the decoder may throw an {@link IllegalStateException} from + * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or + * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + *

+ * See [Internal: b/17933838]. + * + * @param name The name of the decoder. + * @return True if the decoder may throw an exception after receiving an end-of-stream buffer. + */ + private static boolean codecNeedsEosOutputExceptionWorkaround(String name) { + return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name); + } + /** * Returns whether the decoder is known to set the number of audio channels in the output format * to 2 for the given input format, whilst only actually outputting a single channel. diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 550a13771f..814238970b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; /** * A renderer for metadata. @@ -46,17 +47,23 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } private static final int MSG_INVOKE_RENDERER = 0; + // TODO: Holding multiple pending metadata objects is temporary mitigation against + // https://github.com/google/ExoPlayer/issues/1874 + // It should be removed once this issue has been addressed. + private static final int MAX_PENDING_METADATA_COUNT = 5; private final MetadataDecoderFactory decoderFactory; private final Output output; private final Handler outputHandler; private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; + private final Metadata[] pendingMetadata; + private final long[] pendingMetadataTimestamps; + private int pendingMetadataIndex; + private int pendingMetadataCount; private MetadataDecoder decoder; private boolean inputStreamEnded; - private long pendingMetadataTimestamp; - private Metadata pendingMetadata; /** * @param output The output. @@ -87,6 +94,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { this.decoderFactory = Assertions.checkNotNull(decoderFactory); formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); + pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT]; + pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT]; } @Override @@ -101,15 +110,15 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - pendingMetadata = null; + flushPendingMetadata(); inputStreamEnded = false; } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!inputStreamEnded && pendingMetadata == null) { + if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); - int result = readSource(formatHolder, buffer); + int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { inputStreamEnded = true; @@ -118,11 +127,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { // If we ever need to support a metadata format where this is not the case, we'll need to // pass the buffer to the decoder and discard the output. } else { - pendingMetadataTimestamp = buffer.timeUs; buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; buffer.flip(); try { - pendingMetadata = decoder.decode(buffer); + int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = decoder.decode(buffer); + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; } catch (MetadataDecoderException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -130,15 +141,17 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } - if (pendingMetadata != null && pendingMetadataTimestamp <= positionUs) { - invokeRenderer(pendingMetadata); - pendingMetadata = null; + if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { + invokeRenderer(pendingMetadata[pendingMetadataIndex]); + pendingMetadata[pendingMetadataIndex] = null; + pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; + pendingMetadataCount--; } } @Override protected void onDisabled() { - pendingMetadata = null; + flushPendingMetadata(); decoder = null; super.onDisabled(); } @@ -161,6 +174,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } + private void flushPendingMetadata() { + Arrays.fill(pendingMetadata, null); + pendingMetadataIndex = 0; + pendingMetadataCount = 0; + } + @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { @@ -168,8 +187,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { case MSG_INVOKE_RENDERER: invokeRendererInternal((Metadata) msg.obj); return true; + default: + // Should never happen. + throw new IllegalStateException(); } - return false; } private void invokeRendererInternal(Metadata metadata) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 16059ccfbf..cbe6c65030 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -34,6 +34,25 @@ import java.util.Locale; */ public final class Id3Decoder implements MetadataDecoder { + /** + * A predicate for determining whether individual frames should be decoded. + */ + public interface FramePredicate { + + /** + * Returns whether a frame with the specified parameters should be decoded. + * + * @param majorVersion The major version of the ID3 tag. + * @param id0 The first byte of the frame ID. + * @param id1 The second byte of the frame ID. + * @param id2 The third byte of the frame ID. + * @param id3 The fourth byte of the frame ID. + * @return Whether the frame should be decoded. + */ + boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3); + + } + private static final String TAG = "Id3Decoder"; /** @@ -50,6 +69,19 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + private final FramePredicate framePredicate; + + public Id3Decoder() { + this(null); + } + + /** + * @param framePredicate Determines which frames are decoded. May be null to decode all frames. + */ + public Id3Decoder(FramePredicate framePredicate) { + this.framePredicate = framePredicate; + } + @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; @@ -94,7 +126,7 @@ public final class Id3Decoder implements MetadataDecoder { int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { id3Frames.add(frame); } @@ -200,7 +232,7 @@ public final class Id3Decoder implements MetadataDecoder { } private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack, int frameHeaderSize) { + boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -234,6 +266,13 @@ public final class Id3Decoder implements MetadataDecoder { return null; } + if (framePredicate != null + && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) { + // Filtered by the predicate. + id3Data.setPosition(nextFramePosition); + return null; + } + // Frame flags. boolean isCompressed = false; boolean isEncrypted = false; @@ -302,10 +341,10 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeCommentFrame(id3Data, frameSize); } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); } else { String id = majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) @@ -404,6 +443,11 @@ public final class Id3Decoder implements MetadataDecoder { private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize == 0) { + // Frame is empty. + return new PrivFrame("", new byte[0]); + } + byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); @@ -508,8 +552,8 @@ public final class Id3Decoder implements MetadataDecoder { } private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) - throws UnsupportedEncodingException { + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, @@ -531,7 +575,7 @@ public final class Id3Decoder implements MetadataDecoder { int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); } @@ -543,8 +587,8 @@ public final class Id3Decoder implements MetadataDecoder { } private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) - throws UnsupportedEncodingException { + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, @@ -568,7 +612,7 @@ public final class Id3Decoder implements MetadataDecoder { int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 58c23d253a..4050daa1cb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -88,6 +88,9 @@ public final class SpliceInfoDecoder implements MetadataDecoder { case TYPE_PRIVATE_COMMAND: command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment); break; + default: + // Do nothing. + break; } return command == null ? new Metadata() : new Metadata(command); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index b18eabf493..102a689742 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -109,6 +109,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return enablePositionUs - startUs; } + @Override + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs + startUs); + } + @Override public long readDiscontinuity() { if (pendingInitialDiscontinuity) { @@ -231,18 +236,16 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (pendingDiscontinuity) { return C.RESULT_NOTHING_READ; } - if (buffer == null) { - return stream.readData(formatHolder, null); - } if (sentEos) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } - int result = stream.readData(formatHolder, buffer); + int result = stream.readData(formatHolder, buffer, requireFormat); // TODO: Clear gapless playback metadata if a format was read (if applicable). if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ diff --git a/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java new file mode 100644 index 0000000000..eb94351f61 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import java.io.IOException; + +/** + * An empty {@link SampleStream}. + */ +public final class EmptySampleStream implements SampleStream { + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + // Do nothing. + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 5226043593..d843943710 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -234,6 +234,11 @@ import java.io.IOException; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long playbackPositionUs) { if (loadingFinished || (prepared && enabledTrackCount == 0)) { @@ -325,13 +330,14 @@ import java.io.IOException; loader.maybeThrowError(); } - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) { + /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { if (notifyReset || isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished, - lastSeekPositionUs); + return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired, + loadingFinished, lastSeekPositionUs); } // Loader.Callback implementation. @@ -348,6 +354,7 @@ import java.io.IOException; sourceListener.onSourceInfoRefreshed( new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); } + callback.onContinueLoadingRequested(this); } @Override @@ -381,7 +388,7 @@ import java.io.IOException; // ExtractorOutput implementation. Called by the loading thread. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { DefaultTrackOutput trackOutput = sampleQueues.get(id); if (trackOutput == null) { trackOutput = new DefaultTrackOutput(allocator); @@ -552,8 +559,9 @@ import java.io.IOException; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer); + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 31ee8df1e4..3b06542855 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -104,6 +104,13 @@ public interface MediaPeriod extends SequenceableLoader { long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + /** + * Discards buffered media up to the specified position. + * + * @param positionUs The position in microseconds. + */ + void discardBuffer(long positionUs); + /** * Attempts to read a discontinuity. *

diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 10c56e5576..077b5576c1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -128,6 +128,13 @@ import java.util.IdentityHashMap; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (MediaPeriod period : enabledPeriods) { + period.discardBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 417483cebc..6f37165916 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -57,8 +57,7 @@ public final class MergingMediaSource implements MediaSource { * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and * {@link #REASON_PERIOD_COUNT_MISMATCH}. */ - @Reason - public final int reason; + @Reason public final int reason; /** * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index 5ee70cd2ed..e3039878f8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -29,7 +29,8 @@ public interface SampleStream { * Returns whether data is available to be read. *

* Note: If the stream has ended then a buffer with the end of stream flag can always be read from - * {@link #readData(FormatHolder, DecoderInputBuffer)}. Hence an ended stream is always ready. + * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always + * ready. * * @return Whether data is available to be read. */ @@ -45,20 +46,24 @@ public interface SampleStream { /** * Attempts to read from the stream. *

- * If no data is available then {@link C#RESULT_NOTHING_READ} is returned. If the format of the - * media is changing or if {@code buffer == null} then {@code formatHolder} is populated and + * If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code buffer} + * and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then + * {@link C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if + * {@code formatRequired} is set then {@code formatHolder} is populated and * {@link C#RESULT_FORMAT_READ} is returned. Else {@code buffer} is populated and * {@link C#RESULT_BUFFER_READ} is returned. * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the - * caller requires that the format of the stream be read even if it's not changing. + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - int readData(FormatHolder formatHolder, DecoderInputBuffer buffer); + int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired); /** * Attempts to skip to the keyframe before the specified time. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 9aebcece9e..f287153719 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203]. /** * A loader that can proceed in approximate synchronization with other loaders. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index c78bb5371b..5b717e51da 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -111,6 +111,11 @@ import java.util.Arrays; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { @@ -205,14 +210,15 @@ import java.util.Arrays; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (buffer == null || streamState == STREAM_STATE_SEND_FORMAT) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { + if (streamState == STREAM_STATE_END_OF_STREAM) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) { formatHolder.format = format; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; - } else if (streamState == STREAM_STATE_END_OF_STREAM) { - buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; } Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index 0a43ecde63..7a5aeabeb6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -21,14 +21,12 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; /** - * A base implementation of {@link MediaChunk}, for chunks that contain a single track. - *

- * Loaded samples are output to a {@link DefaultTrackOutput}. + * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}. */ public abstract class BaseMediaChunk extends MediaChunk { - private DefaultTrackOutput trackOutput; - private int firstSampleIndex; + private BaseMediaChunkOutput output; + private int[] firstSampleIndices; /** * @param dataSource The source from which the data should be loaded. @@ -48,29 +46,29 @@ public abstract class BaseMediaChunk extends MediaChunk { } /** - * Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive + * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive * samples as they are loaded. * - * @param trackOutput The output that will receive the loaded samples. + * @param output The output that will receive the loaded media samples. */ - public void init(DefaultTrackOutput trackOutput) { - this.trackOutput = trackOutput; - this.firstSampleIndex = trackOutput.getWriteIndex(); + public void init(BaseMediaChunkOutput output) { + this.output = output; + firstSampleIndices = output.getWriteIndices(); } /** - * Returns the index of the first sample in the output that was passed to - * {@link #init(DefaultTrackOutput)} that will originate from this chunk. + * Returns the index of the first sample in the specified track of the output that will originate + * from this chunk. */ - public final int getFirstSampleIndex() { - return firstSampleIndex; + public final int getFirstSampleIndex(int trackIndex) { + return firstSampleIndices[trackIndex]; } /** - * Returns the track output most recently passed to {@link #init(DefaultTrackOutput)}. + * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}. */ - protected final DefaultTrackOutput getTrackOutput() { - return trackOutput; + protected final BaseMediaChunkOutput getOutput() { + return output; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java new file mode 100644 index 0000000000..3882a330f9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.chunk; + +import android.util.Log; +import com.google.android.exoplayer2.extractor.DefaultTrackOutput; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; + +/** + * An output for {@link BaseMediaChunk}s. + */ +/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider { + + private static final String TAG = "BaseMediaChunkOutput"; + + private final int[] trackTypes; + private final DefaultTrackOutput[] trackOutputs; + + /** + * @param trackTypes The track types of the individual track outputs. + * @param trackOutputs The individual track outputs. + */ + public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) { + this.trackTypes = trackTypes; + this.trackOutputs = trackOutputs; + } + + @Override + public TrackOutput track(int id, int type) { + for (int i = 0; i < trackTypes.length; i++) { + if (type == trackTypes[i]) { + return trackOutputs[i]; + } + } + Log.e(TAG, "Unmatched track of type: " + type); + return new DummyTrackOutput(); + } + + /** + * Returns the current absolute write indices of the individual track outputs. + */ + public int[] getWriteIndices() { + int[] writeIndices = new int[trackOutputs.length]; + for (int i = 0; i < trackOutputs.length; i++) { + if (trackOutputs[i] != null) { + writeIndices[i] = trackOutputs[i].getWriteIndex(); + } + } + return writeIndices; + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently written to the track outputs. + */ + public void setSampleOffsetUs(long sampleOffsetUs) { + for (DefaultTrackOutput trackOutput : trackOutputs) { + if (trackOutput != null) { + trackOutput.setSampleOffsetUs(sampleOffsetUs); + } + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 2623d31cef..501f4998cf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -15,9 +15,10 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -30,72 +31,79 @@ import java.io.IOException; /** * An {@link Extractor} wrapper for loading chunks containing a single track. *

- * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive - * parsed data. + * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. */ -public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { +public final class ChunkExtractorWrapper implements ExtractorOutput { /** - * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}. + * Provides {@link TrackOutput} instances to be written to by the wrapper. */ - public interface SeekMapOutput { + public interface TrackOutputProvider { /** - * @see ExtractorOutput#seekMap(SeekMap) + * Called to get the {@link TrackOutput} for a specific track. + *

+ * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. + * + * @param id A track identifier. + * @param type The type of the track. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @return The {@link TrackOutput} for the given track identifier. */ - void seekMap(SeekMap seekMap); + TrackOutput track(int id, int type); } public final Extractor extractor; private final Format manifestFormat; - private final boolean preferManifestDrmInitData; - private final boolean resendFormatOnInit; + private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; - private SeekMapOutput seekMapOutput; - private TrackOutput trackOutput; - private Format sentFormat; - - // Accessed only on the loader thread. - private boolean seenTrack; - private int seenTrackId; + private TrackOutputProvider trackOutputProvider; + private SeekMap seekMap; + private Format[] sampleFormats; /** * @param extractor The extractor to wrap. * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any * sample {@link Format} output from the {@link Extractor}. - * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} - * should be preferred when the sample and manifest {@link Format}s are merged. - * @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when - * it is initialized via {@link #init(SeekMapOutput, TrackOutput)}. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, - boolean preferManifestDrmInitData, boolean resendFormatOnInit) { + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) { this.extractor = extractor; this.manifestFormat = manifestFormat; - this.preferManifestDrmInitData = preferManifestDrmInitData; - this.resendFormatOnInit = resendFormatOnInit; + bindingTrackOutputs = new SparseArray<>(); } /** - * Initializes the extractor to output to the provided {@link SeekMapOutput} and - * {@link TrackOutput} instances, and configures it to receive data from a new chunk. - * - * @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s. - * @param trackOutput The {@link TrackOutput} that will receive sample data. + * Returns the {@link SeekMap} most recently output by the extractor, or null. */ - public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) { - this.seekMapOutput = seekMapOutput; - this.trackOutput = trackOutput; + public SeekMap getSeekMap() { + return seekMap; + } + + /** + * Returns the sample {@link Format}s most recently output by the extractor, or null. + */ + public Format[] getSampleFormats() { + return sampleFormats; + } + + /** + * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to + * receive data from a new chunk. + * + * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. + */ + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; if (!extractorInitialized) { extractor.init(this); extractorInitialized = true; } else { extractor.seek(0, 0); - if (resendFormatOnInit && sentFormat != null) { - trackOutput.format(sentFormat); + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); } } } @@ -103,46 +111,85 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput // ExtractorOutput implementation. @Override - public TrackOutput track(int id) { - Assertions.checkState(!seenTrack || seenTrackId == id); - seenTrack = true; - seenTrackId = id; - return this; + public TrackOutput track(int id, int type) { + BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id); + if (bindingTrackOutput == null) { + // Assert that if we're seeing a new track we have not seen endTracks. + Assertions.checkState(sampleFormats == null); + bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat); + bindingTrackOutput.bind(trackOutputProvider); + bindingTrackOutputs.put(id, bindingTrackOutput); + } + return bindingTrackOutput; } @Override public void endTracks() { - Assertions.checkState(seenTrack); + Format[] sampleFormats = new Format[bindingTrackOutputs.size()]; + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat; + } + this.sampleFormats = sampleFormats; } @Override public void seekMap(SeekMap seekMap) { - seekMapOutput.seekMap(seekMap); + this.seekMap = seekMap; } - // TrackOutput implementation. + // Internal logic. - @Override - public void format(Format format) { - sentFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); - trackOutput.format(sentFormat); - } + private static final class BindingTrackOutput implements TrackOutput { - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - return trackOutput.sampleData(input, length, allowEndOfInput); - } + private final int id; + private final int type; + private final Format manifestFormat; - @Override - public void sampleData(ParsableByteArray data, int length) { - trackOutput.sampleData(data, length); - } + public Format sampleFormat; + private TrackOutput trackOutput; + + public BindingTrackOutput(int id, int type, Format manifestFormat) { + this.id = id; + this.type = type; + this.manifestFormat = manifestFormat; + } + + public void bind(TrackOutputProvider trackOutputProvider) { + if (trackOutputProvider == null) { + trackOutput = new DummyTrackOutput(); + return; + } + trackOutput = trackOutputProvider.track(id, type); + if (trackOutput != null) { + trackOutput.format(sampleFormat); + } + } + + @Override + public void format(Format format) { + // TODO: This should only happen for the primary track. Additional metadata/text tracks need + // to be copied with different manifest derived formats. + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + trackOutput.format(sampleFormat); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return trackOutput.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + trackOutput.sampleData(data, length); + } + + @Override + public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, + byte[] encryptionKey) { + trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + } - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 3955d64034..93d86a8de1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -33,30 +33,35 @@ import java.util.List; /** * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. + * May also be configured to expose additional embedded {@link SampleStream}s. */ public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback { - private final int trackType; + private final int primaryTrackType; + private final int[] embeddedTrackTypes; + private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; private final int minLoadableRetryCount; + private final Loader loader; + private final ChunkHolder nextChunkHolder; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; - private final DefaultTrackOutput sampleQueue; - private final ChunkHolder nextChunkHolder; - private final Loader loader; + private final DefaultTrackOutput primarySampleQueue; + private final DefaultTrackOutput[] embeddedSampleQueues; + private final BaseMediaChunkOutput mediaChunkOutput; - private Format downstreamTrackFormat; - - private long lastSeekPositionUs; + private Format primaryDownstreamTrackFormat; private long pendingResetPositionUs; - - private boolean loadingFinished; + /* package */ long lastSeekPositionUs; + /* package */ boolean loadingFinished; /** - * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param primaryTrackType The type of the primary track. One of the {@link C} + * {@code TRACK_TYPE_*} constants. + * @param embeddedTrackTypes The types of any embedded tracks, or null. * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. @@ -65,10 +70,11 @@ public class ChunkSampleStream implements SampleStream, S * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkSampleStream(int trackType, T chunkSource, - SequenceableLoader.Callback> callback, Allocator allocator, - long positionUs, int minLoadableRetryCount, EventDispatcher eventDispatcher) { - this.trackType = trackType; + public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource, + Callback> callback, Allocator allocator, long positionUs, + int minLoadableRetryCount, EventDispatcher eventDispatcher) { + this.primaryTrackType = primaryTrackType; + this.embeddedTrackTypes = embeddedTrackTypes; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; @@ -77,15 +83,68 @@ public class ChunkSampleStream implements SampleStream, S nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); - sampleQueue = new DefaultTrackOutput(allocator); - lastSeekPositionUs = positionUs; + + int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedTracksSelected = new boolean[embeddedTrackCount]; + int[] trackTypes = new int[1 + embeddedTrackCount]; + DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; + + primarySampleQueue = new DefaultTrackOutput(allocator); + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + + for (int i = 0; i < embeddedTrackCount; i++) { + DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); + embeddedSampleQueues[i] = trackOutput; + sampleQueues[i + 1] = trackOutput; + trackTypes[i + 1] = embeddedTrackTypes[i]; + } + + mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; + lastSeekPositionUs = positionUs; + } + + /** + * Discards buffered media for embedded tracks that are not currently selected, up to the + * specified position. + * + * @param positionUs The position to discard up to, in microseconds. + */ + public void discardUnselectedEmbeddedTracksTo(long positionUs) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (!embeddedTracksSelected[i]) { + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + } + } + } + + /** + * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's + * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned + * stream when the track is no longer required, and before calling this method again to obtain + * another stream for the same track. + * + * @param positionUs The current playback position in microseconds. + * @param trackType The type of the embedded track to enable. + * @return The {@link EmbeddedSampleStream} for the embedded track. + */ + public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (embeddedTrackTypes[i] == trackType) { + Assertions.checkState(!embeddedTracksSelected[i]); + embeddedTracksSelected[i] = true; + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); + } + } + // Should never happen. + throw new IllegalStateException(); } /** * Returns the {@link ChunkSource} used by this stream. - * - * @return The {@link ChunkSource}. */ public T getChunkSource() { return chunkSource; @@ -110,7 +169,7 @@ public class ChunkSampleStream implements SampleStream, S if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - return Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs()); + return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs()); } } @@ -121,15 +180,21 @@ public class ChunkSampleStream implements SampleStream, S */ public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; - // If we're not pending a reset, see if we can seek within the sample queue. - boolean seekInsideBuffer = !isPendingReset() - && sampleQueue.skipToKeyframeBefore(positionUs, positionUs < getNextLoadPositionUs()); + // If we're not pending a reset, see if we can seek within the primary sample queue. + boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore( + positionUs, positionUs < getNextLoadPositionUs()); if (seekInsideBuffer) { - // We succeeded. All we need to do is discard any chunks that we've moved past. + // We succeeded. We need to discard any chunks that we've moved past and perform the seek for + // any embedded streams as well. while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { mediaChunks.removeFirst(); } + // TODO: For this to work correctly, the embedded streams must not discard anything from their + // sample queues beyond the current read position of the primary stream. + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.skipToKeyframeBefore(positionUs); + } } else { // We failed, and need to restart. pendingResetPositionUs = positionUs; @@ -138,7 +203,10 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading()) { loader.cancelLoading(); } else { - sampleQueue.reset(true); + primarySampleQueue.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); + } } } } @@ -149,7 +217,10 @@ public class ChunkSampleStream implements SampleStream, S * This method should be called when the stream is no longer required. */ public void release() { - sampleQueue.disable(); + primarySampleQueue.disable(); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.disable(); + } loader.release(); } @@ -157,7 +228,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty()); } @Override @@ -169,30 +240,19 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { - mediaChunks.removeFirst(); - } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(downstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(trackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - downstreamTrackFormat = trackFormat; - return sampleQueue.readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); + discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); + return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); } @Override public void skipToKeyframeBefore(long timeUs) { - sampleQueue.skipToKeyframeBefore(timeUs); + primarySampleQueue.skipToKeyframeBefore(timeUs); } // Loader.Callback implementation. @@ -200,20 +260,25 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded()); callback.onContinueLoadingRequested(this); } @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded()); if (!released) { - sampleQueue.reset(true); + primarySampleQueue.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); + } callback.onContinueLoadingRequested(this); } } @@ -230,16 +295,19 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk) { BaseMediaChunk removed = mediaChunks.removeLast(); Assertions.checkState(removed == loadable); - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } } } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, error, - canceled); + eventDispatcher.loadError(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, + error, canceled); if (canceled) { callback.onContinueLoadingRequested(this); return Loader.DONT_RETRY; @@ -275,13 +343,13 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk(loadable)) { pendingResetPositionUs = C.TIME_UNSET; BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; - mediaChunk.init(sampleQueue); + mediaChunk.init(mediaChunkOutput); mediaChunks.add(mediaChunk); } long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs); + eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); return true; } @@ -312,10 +380,25 @@ public class ChunkSampleStream implements SampleStream, S return chunk instanceof BaseMediaChunk; } - private boolean isPendingReset() { + /* package */ boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } + private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { + while (mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + mediaChunks.removeFirst(); + } + BaseMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * @@ -328,16 +411,67 @@ public class ChunkSampleStream implements SampleStream, S } long startTimeUs = 0; long endTimeUs = mediaChunks.getLast().endTimeUs; - BaseMediaChunk removed = null; while (mediaChunks.size() > queueLength) { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; loadingFinished = false; } - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); - eventDispatcher.upstreamDiscarded(trackType, startTimeUs, endTimeUs); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } + eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } + /** + * A {@link SampleStream} embedded in a {@link ChunkSampleStream}. + */ + public final class EmbeddedSampleStream implements SampleStream { + + public final ChunkSampleStream parent; + + private final DefaultTrackOutput sampleQueue; + private final int index; + + public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, + int index) { + this.parent = parent; + this.sampleQueue = sampleQueue; + this.index = index; + } + + @Override + public boolean isReady() { + return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + sampleQueue.skipToKeyframeBefore(timeUs); + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. Errors will be thrown from the primary stream. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); + } + + public void release() { + Assertions.checkState(embeddedTracksSelected[index]); + embeddedTracksSelected[index] = false; + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 060e6130cf..cfbefc0c2e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -17,11 +17,8 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -31,12 +28,11 @@ import java.io.IOException; /** * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. */ -public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput { +public class ContainerMediaChunk extends BaseMediaChunk { private final int chunkCount; private final long sampleOffsetUs; private final ChunkExtractorWrapper extractorWrapper; - private final Format sampleFormat; private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -56,19 +52,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. - * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if - * the data is known to define its own sample format. */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, - Format sampleFormat) { + int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; this.extractorWrapper = extractorWrapper; - this.sampleFormat = sampleFormat; } @Override @@ -86,13 +78,6 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput return bytesLoaded; } - // SeekMapOutput implementation. - - @Override - public final void seekMap(SeekMap seekMap) { - // Do nothing. - } - // Loadable implementation. @Override @@ -114,10 +99,10 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, sampleOffsetUs); - extractorWrapper.init(this, trackOutput); + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init(output); } // Load and decode the sample data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index c8c3389830..69474aa150 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -20,30 +20,19 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. */ -public final class InitializationChunk extends Chunk implements SeekMapOutput, - TrackOutput { +public final class InitializationChunk extends Chunk { private final ChunkExtractorWrapper extractorWrapper; - // Initialization results. Set by the loader thread and read by any thread that knows loading - // has completed. These variables do not need to be volatile, since a memory barrier must occur - // for the reading thread to know that loading has completed. - private Format sampleFormat; - private SeekMap seekMap; - private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -68,55 +57,6 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, return bytesLoaded; } - /** - * Returns a {@link Format} parsed from the chunk, or null. - *

- * Should be called after loading has completed. - */ - public Format getSampleFormat() { - return sampleFormat; - } - - /** - * Returns a {@link SeekMap} parsed from the chunk, or null. - *

- * Should be called after loading has completed. - */ - public SeekMap getSeekMap() { - return seekMap; - } - - // SeekMapOutput implementation. - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - } - - // TrackOutput implementation. - - @Override - public void format(Format format) { - this.sampleFormat = format; - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleData(ParsableByteArray data, int length) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - // Loadable implementation. @Override @@ -138,8 +78,7 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - extractorWrapper.init(this, this); + extractorWrapper.init(null); } // Load and decode the initialization data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index d7be74535e..a008c9cd84 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Util; @@ -30,6 +30,7 @@ import java.io.IOException; */ public final class SingleSampleMediaChunk extends BaseMediaChunk { + private final int trackType; private final Format sampleFormat; private volatile int bytesLoaded; @@ -45,15 +46,20 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param chunkIndex The index of the chunk. + * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @param sampleFormat The {@link Format} of the sample in the chunk. */ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, Format sampleFormat) { + int chunkIndex, int trackType, Format sampleFormat) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.trackType = trackType; this.sampleFormat = sampleFormat; } + @Override public boolean isLoadCompleted() { return loadCompleted; @@ -87,8 +93,10 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { length += bytesLoaded; } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); - DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, 0); + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(0); + TrackOutput trackOutput = output.track(0, trackType); + trackOutput.format(sampleFormat); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 4c943abb48..72f728092c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -29,7 +29,8 @@ public interface DashChunkSource extends ChunkSource { DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs); + TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 4a24c7c176..5e0541cb31 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -15,25 +15,30 @@ */ package com.google.android.exoplayer2.source.dash; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; +import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -50,21 +55,22 @@ import java.util.List; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; private final TrackGroupArray trackGroups; + private final EmbeddedTrackInfo[] embeddedTrackInfos; private Callback callback; private ChunkSampleStream[] sampleStreams; private CompositeSequenceableLoader sequenceableLoader; private DashManifest manifest; - private int index; - private Period period; + private int periodIndex; + private List adaptationSets; - public DashMediaPeriod(int id, DashManifest manifest, int index, + public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { this.id = id; this.manifest = manifest; - this.index = index; + this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; @@ -73,17 +79,19 @@ import java.util.List; this.allocator = allocator; sampleStreams = newSampleStreamArray(0); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - period = manifest.getPeriod(index); - trackGroups = buildTrackGroups(period); + adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + Pair result = buildTrackGroups(adaptationSets); + trackGroups = result.first; + embeddedTrackInfos = result.second; } - public void updateManifest(DashManifest manifest, int index) { + public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; - this.index = index; - period = manifest.getPeriod(index); + this.periodIndex = periodIndex; + adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; if (sampleStreams != null) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.getChunkSource().updateManifest(manifest, index); + sampleStream.getChunkSource().updateManifest(manifest, periodIndex); } callback.onContinueLoadingRequested(this); } @@ -114,31 +122,75 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - ArrayList> sampleStreamsList = new ArrayList<>(); + int adaptationSetCount = adaptationSets.size(); + HashMap> primarySampleStreams = new HashMap<>(); + // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] != null) { + if (streams[i] instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { stream.release(); streams[i] = null; } else { - sampleStreamsList.add(stream); + int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + primarySampleStreams.put(adaptationSetIndex, stream); } } if (streams[i] == null && selections[i] != null) { - ChunkSampleStream stream = buildSampleStream(selections[i], positionUs); - sampleStreamsList.add(stream); - streams[i] = stream; - streamResetFlags[i] = true; + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (trackGroupIndex < adaptationSetCount) { + ChunkSampleStream stream = buildSampleStream(trackGroupIndex, + selections[i], positionUs); + primarySampleStreams.put(trackGroupIndex, stream); + streams[i] = stream; + streamResetFlags[i] = true; + } } } - sampleStreams = newSampleStreamArray(sampleStreamsList.size()); - sampleStreamsList.toArray(sampleStreams); + // Second pass for embedded tracks. + for (int i = 0; i < selections.length; i++) { + if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) + && (selections[i] == null || !mayRetainStreamFlags[i])) { + // The stream is for an embedded track and is either no longer selected or needs replacing. + releaseIfEmbeddedSampleStream(streams[i]); + streams[i] = null; + } + // We need to consider replacing the stream even if it's non-null because the primary stream + // may have been replaced, selected or deselected. + if (selections[i] != null) { + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (trackGroupIndex >= adaptationSetCount) { + int embeddedTrackIndex = trackGroupIndex - adaptationSetCount; + EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex]; + int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; + ChunkSampleStream primaryStream = primarySampleStreams.get(adaptationSetIndex); + SampleStream stream = streams[i]; + boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream + : (stream instanceof EmbeddedSampleStream + && ((EmbeddedSampleStream) stream).parent == primaryStream); + if (!mayRetainStream) { + releaseIfEmbeddedSampleStream(stream); + streams[i] = primaryStream == null ? new EmptySampleStream() + : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType); + streamResetFlags[i] = true; + } + } + } + } + sampleStreams = newSampleStreamArray(primarySampleStreams.size()); + primarySampleStreams.values().toArray(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardUnselectedEmbeddedTracksTo(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -183,29 +235,98 @@ import java.util.List; // Internal methods. - private static TrackGroupArray buildTrackGroups(Period period) { - TrackGroup[] trackGroupArray = new TrackGroup[period.adaptationSets.size()]; - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); + private static Pair buildTrackGroups( + List adaptationSets) { + int adaptationSetCount = adaptationSets.size(); + int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets); + TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount]; + EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount]; + + int embeddedTrackIndex = 0; + for (int i = 0; i < adaptationSetCount; i++) { + AdaptationSet adaptationSet = adaptationSets.get(i); List representations = adaptationSet.representations; Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { formats[j] = representations.get(j).format; } trackGroupArray[i] = new TrackGroup(formats); + if (hasEventMessageTrack(adaptationSet)) { + Format format = Format.createSampleFormat(adaptationSet.id + ":emsg", + MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); + embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA); + } + if (hasCea608Track(adaptationSet)) { + Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608", + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null); + trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); + embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT); + } } - return new TrackGroupArray(trackGroupArray); + + return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos); } - private ChunkSampleStream buildSampleStream(TrackSelection selection, - long positionUs) { - int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup()); - AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + private ChunkSampleStream buildSampleStream(int adaptationSetIndex, + TrackSelection selection, long positionUs) { + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + int embeddedTrackCount = 0; + int[] embeddedTrackTypes = new int[2]; + boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); + if (enableEventMessageTrack) { + embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; + } + boolean enableCea608Track = hasCea608Track(adaptationSet); + if (enableCea608Track) { + embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; + } + if (embeddedTrackCount < embeddedTrackTypes.length) { + embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); + } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection, - elapsedRealtimeOffset); - return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, - minLoadableRetryCount, eventDispatcher); + manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection, + elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); + ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type, + embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, + eventDispatcher); + return stream; + } + + private static int getEmbeddedTrackCount(List adaptationSets) { + int embeddedTrackCount = 0; + for (int i = 0; i < adaptationSets.size(); i++) { + AdaptationSet adaptationSet = adaptationSets.get(i); + if (hasEventMessageTrack(adaptationSet)) { + embeddedTrackCount++; + } + if (hasCea608Track(adaptationSet)) { + embeddedTrackCount++; + } + } + return embeddedTrackCount; + } + + private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) { + List representations = adaptationSet.representations; + for (int i = 0; i < representations.size(); i++) { + Representation representation = representations.get(i); + if (!representation.inbandEventStreams.isEmpty()) { + return true; + } + } + return false; + } + + private static boolean hasCea608Track(AdaptationSet adaptationSet) { + List descriptors = adaptationSet.accessibilityDescriptors; + for (int i = 0; i < descriptors.size(); i++) { + SchemeValuePair descriptor = descriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { + return true; + } + } + return false; } @SuppressWarnings("unchecked") @@ -213,4 +334,22 @@ import java.util.List; return new ChunkSampleStream[length]; } + private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { + if (sampleStream instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) sampleStream).release(); + } + } + + private static final class EmbeddedTrackInfo { + + public final int adaptationSetIndex; + public final int trackType; + + public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) { + this.adaptationSetIndex = adaptationSetIndex; + this.trackType = trackType; + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 99845c057e..eec99521f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -572,22 +572,28 @@ public final class DashMediaSource implements MediaSource { long availableStartTimeUs = 0; long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; + boolean seenEmptyIndex = false; for (int i = 0; i < adaptationSetCount; i++) { DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } - int firstSegmentNum = index.getFirstSegmentNum(); - int lastSegmentNum = index.getLastSegmentNum(durationUs); isIndexExplicit |= index.isExplicit(); - long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); - availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); - if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { - long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) - + index.getDurationUs(lastSegmentNum, durationUs); - availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); - } else { - // The available end time is unmodified, because this index is unbounded. + int segmentCount = index.getSegmentCount(durationUs); + if (segmentCount == 0) { + seenEmptyIndex = true; + availableStartTimeUs = 0; + availableEndTimeUs = 0; + } else if (!seenEmptyIndex) { + int firstSegmentNum = index.getFirstSegmentNum(); + long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); + availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) { + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) + + index.getDurationUs(lastSegmentNum, durationUs); + availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + } } } return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); @@ -704,8 +710,8 @@ public final class DashMediaSource implements MediaSource { // not correspond to the start of a segment in both, but this is an edge case. DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) .representations.get(0).getIndex(); - if (snapIndex == null) { - // Video adaptation set does not include an index for snapping. + if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) { + // Video adaptation set does not include a non-empty index for snapping. return windowDefaultStartPositionUs; } int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java index d002831c4f..2ddc7f4f80 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java @@ -26,12 +26,10 @@ public interface DashSegmentIndex { int INDEX_UNBOUNDED = -1; /** - * Returns the segment number of the segment containing a given media time. - *

- * If the given media time is outside the range of the index, then the returned segment number is - * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the - * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the - * end of the last segment). + * Returns {@code getFirstSegmentNum()} if the index has no segments or if the given media time is + * earlier than the start of the first segment. Returns {@code getFirstSegmentNum() + + * getSegmentCount() - 1} if the given media time is later than the end of the last segment. + * Otherwise, returns the segment number of the segment containing the given media time. * * @param timeUs The time in microseconds. * @param periodDurationUs The duration of the enclosing period in microseconds, or @@ -74,7 +72,7 @@ public interface DashSegmentIndex { int getFirstSegmentNum(); /** - * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}. *

* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a * SegmentTimeline element, and if the period duration is not yet known. In this case the caller @@ -82,9 +80,9 @@ public interface DashSegmentIndex { * * @param periodDurationUs The duration of the enclosing period in microseconds, or * {@link C#TIME_UNSET} if the period's duration is not yet known. - * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. */ - int getLastSegmentNum(long periodDurationUs); + int getSegmentCount(long periodDurationUs); /** * Returns true if segments are defined explicitly by the index. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java new file mode 100644 index 0000000000..8fca21b2e0 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.dash; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; +import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.RangedUri; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; + +/** + * Utility methods for DASH streams. + */ +public final class DashUtil { + + /** + * Loads a DASH manifest. + * + * @param dataSource The {@link HttpDataSource} from which the manifest should be read. + * @param manifestUriString The URI of the manifest to be read. + * @return An instance of {@link DashManifest}. + * @throws IOException If an error occurs reading data from the stream. + * @see DashManifestParser + */ + public static DashManifest loadManifest(DataSource dataSource, String manifestUriString) + throws IOException { + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(Uri.parse(manifestUriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + DashManifestParser parser = new DashManifestParser(); + return parser.parse(dataSource.getUri(), inputStream); + } finally { + inputStream.close(); + } + } + + /** + * Loads initialization data for the {@code representation} and returns the sample {@link + * Format}. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @return the sample {@link Format} of the given representation. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static Format loadSampleFormat(DataSource dataSource, Representation representation) + throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, + false); + return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; + } + + /** + * Loads initialization and index data for the {@code representation} and returns the {@link + * ChunkIndex}. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @return {@link ChunkIndex} of the given representation. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation) + throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, + true); + return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); + } + + /** + * Loads initialization data for the {@code representation} and optionally index data then + * returns a {@link ChunkExtractorWrapper} which contains the output. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @param loadIndex Whether to load index data too. + * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no + * initialization or (if requested) index data exists. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, + Representation representation, boolean loadIndex) + throws IOException, InterruptedException { + RangedUri initializationUri = representation.getInitializationUri(); + if (initializationUri == null) { + return null; + } + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + RangedUri requestUri; + if (loadIndex) { + RangedUri indexUri = representation.getIndexUri(); + if (indexUri == null) { + return null; + } + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + requestUri = initializationUri.attemptMerge(indexUri, representation.baseUrl); + if (requestUri == null) { + loadInitializationData(dataSource, representation, extractorWrapper, initializationUri); + requestUri = indexUri; + } + } else { + requestUri = initializationUri; + } + loadInitializationData(dataSource, representation, extractorWrapper, requestUri); + return extractorWrapper; + } + + private static void loadInitializationData(DataSource dataSource, + Representation representation, ChunkExtractorWrapper extractorWrapper, RangedUri requestUri) + throws IOException, InterruptedException { + DataSpec dataSpec = new DataSpec(requestUri.resolveUri(representation.baseUrl), + requestUri.start, requestUri.length, representation.getCacheKey()); + InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, + representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, + extractorWrapper); + initializationChunk.load(); + } + + private static ChunkExtractorWrapper newWrappedExtractor(Format format) { + String mimeType = format.containerMimeType; + boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) + || mimeType.startsWith(MimeTypes.AUDIO_WEBM); + Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); + return new ChunkExtractorWrapper(extractor, format); + } + + private DashUtil() {} + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 56ea626120..40f3448f6a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return chunkIndex.length - 1; + public int getSegmentCount(long periodDurationUs) { + return chunkIndex.length; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 74d53d3e32..7ccea8a2a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -69,11 +70,12 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs) { + TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, - maxSegmentsPerLoad); + maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); } } @@ -105,10 +107,15 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. * Note that segments will only be combined if their {@link Uri}s are the same and if their * data ranges are adjacent. + * @param enableEventMessageTrack Whether the chunks generated by the source may output an event + * message track. + * @param enableEventMessageTrack Whether the chunks generated by the source may output a CEA-608 + * track. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad) { + DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, + boolean enableEventMessageTrack, boolean enableCea608Track) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndex = adaptationSetIndex; @@ -119,11 +126,13 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + AdaptationSet adaptationSet = getAdaptationSet(); + List representations = adaptationSet.representations; representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, + enableEventMessageTrack, enableCea608Track, adaptationSet.type); } } @@ -133,7 +142,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + List representations = getAdaptationSet().representations; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -176,8 +185,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; - Format sampleFormat = representationHolder.sampleFormat; - if (sampleFormat == null) { + if (representationHolder.extractorWrapper.getSampleFormats() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } if (segmentIndex == null) { @@ -192,10 +200,16 @@ public class DefaultDashChunkSource implements DashChunkSource { } long nowUs = getNowUnixTimeUs(); + int availableSegmentCount = representationHolder.getSegmentCount(); + if (availableSegmentCount == 0) { + // The index doesn't define any segments. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { + int lastAvailableSegmentNum; + if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; @@ -209,6 +223,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; + } else { + lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } int segmentNum; @@ -233,8 +249,8 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat, - segmentNum, maxSegmentCount); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, + maxSegmentCount); } @Override @@ -243,15 +259,11 @@ public class DefaultDashChunkSource implements DashChunkSource { InitializationChunk initializationChunk = (InitializationChunk) chunk; RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; - Format sampleFormat = initializationChunk.getSampleFormat(); - if (sampleFormat != null) { - representationHolder.setSampleFormat(sampleFormat); - } // The null check avoids overwriting an index obtained from the manifest with one obtained // from the stream. If the manifest defines an index then the stream shouldn't, but in cases // where it does we should ignore it. if (representationHolder.segmentIndex == null) { - SeekMap seekMap = initializationChunk.getSeekMap(); + SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); } @@ -270,10 +282,13 @@ public class DefaultDashChunkSource implements DashChunkSource { && ((InvalidResponseCodeException) e).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { - missingLastSegment = true; - return true; + int segmentCount = representationHolder.getSegmentCount(); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { + int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } } } // Blacklist if appropriate. @@ -283,8 +298,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private List getRepresentations() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; + private AdaptationSet getAdaptationSet() { + return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); } private long getNowUnixTimeUs() { @@ -318,7 +333,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { + Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -328,7 +343,8 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, + representationHolder.trackType, trackFormat); } else { int segmentCount = 1; for (int i = 1; i < maxSegmentCount; i++) { @@ -347,7 +363,7 @@ public class DefaultDashChunkSource implements DashChunkSource { long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, - sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); + sampleOffsetUs, representationHolder.extractorWrapper); } } @@ -355,45 +371,46 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { + public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; - public Format sampleFormat; private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation) { + public RepresentationHolder(long periodDurationUs, Representation representation, + boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; + this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; } else { - boolean resendFormatOnInit = false; Extractor extractor; if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { extractor = new RawCcExtractor(representation.format); - resendFormatOnInit = true; } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { - extractor = new FragmentedMp4Extractor(); + int flags = 0; + if (enableEventMessageTrack) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; + } + if (enableCea608Track) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; + } + extractor = new FragmentedMp4Extractor(flags); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, - representation.format, true /* preferManifestDrmInitData */, - resendFormatOnInit); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format); } segmentIndex = representation.getIndex(); } - public void setSampleFormat(Format sampleFormat) { - this.sampleFormat = sampleFormat; - } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) throws BehindLiveWindowException{ DashSegmentIndex oldIndex = representation.getIndex(); @@ -412,15 +429,20 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); + int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + if (oldIndexSegmentCount == 0) { + // Segment numbers cannot shift if the old index was empty. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - - newIndexFirstSegmentNum; + segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. @@ -436,12 +458,8 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getFirstSegmentNum() + segmentNumShift; } - public int getLastSegmentNum() { - int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); - if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } - return lastSegmentNum + segmentNumShift; + public int getSegmentCount() { + return segmentIndex.getSegmentCount(periodDurationUs); } public long getSegmentStartTimeUs(int segmentNum) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 0c713b949a..eb51c8312d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; /** @@ -79,4 +81,64 @@ public class DashManifest { return C.msToUs(getPeriodDurationMs(index)); } + /** + * Creates a copy of this manifest which includes only the representations identified by the given + * keys. + * + * @param representationKeys List of keys for the representations to be included in the copy. + * @return A copy of this manifest with the selected representations. + * @throws IndexOutOfBoundsException If a key has an invalid index. + */ + public final DashManifest copy(List representationKeys) { + LinkedList keys = new LinkedList<>(representationKeys); + Collections.sort(keys); + keys.add(new RepresentationKey(-1, -1, -1)); // Add a stopper key to the end + + ArrayList copyPeriods = new ArrayList<>(); + long shiftMs = 0; + for (int periodIndex = 0; periodIndex < getPeriodCount(); periodIndex++) { + if (keys.peek().periodIndex != periodIndex) { + // No representations selected in this period. + long periodDurationMs = getPeriodDurationMs(periodIndex); + if (periodDurationMs != C.TIME_UNSET) { + shiftMs += periodDurationMs; + } + } else { + Period period = getPeriod(periodIndex); + ArrayList copyAdaptationSets = + copyAdaptationSets(period.adaptationSets, keys); + copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); + } + } + long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; + return new DashManifest(availabilityStartTime, newDuration, minBufferTime, dynamic, + minUpdatePeriod, timeShiftBufferDepth, suggestedPresentationDelay, utcTiming, location, + copyPeriods); + } + + private static ArrayList copyAdaptationSets( + List adaptationSets, LinkedList keys) { + RepresentationKey key = keys.poll(); + int periodIndex = key.periodIndex; + ArrayList copyAdaptationSets = new ArrayList<>(); + do { + int adaptationSetIndex = key.adaptationSetIndex; + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + + List representations = adaptationSet.representations; + ArrayList copyRepresentations = new ArrayList<>(); + do { + Representation representation = representations.get(key.representationIndex); + copyRepresentations.add(representation); + key = keys.poll(); + } while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex); + + copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, + copyRepresentations, adaptationSet.accessibilityDescriptors)); + } while(key.periodIndex == periodIndex); + // Add back the last key which doesn't belong to the period being processed + keys.addFirst(key); + return copyAdaptationSets; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 1917399282..d4338fd812 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -335,30 +335,35 @@ public class DashManifestParser extends DefaultHandler */ protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); byte[] data = null; UUID uuid = null; - boolean seenPsshElement = false; boolean requiresSecureDecoder = false; do { xpp.next(); - // The cenc:pssh element is defined in 23001-7:2015. - if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - seenPsshElement = true; + if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") + && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. data = Base64.decode(xpp.getText(), Base64.DEFAULT); uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + data = null; + } + } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, + Base64.decode(xpp.getText(), Base64.DEFAULT)); + uuid = C.PLAYREADY_UUID; } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - if (!seenPsshElement) { - return null; - } else if (uuid != null) { - return new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder); - } else { - Log.w(TAG, "Skipped unsupported ContentProtection element"); - return null; - } + return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 4146037e1c..5960d4d7ba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -318,8 +318,8 @@ public abstract class Representation { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return segmentBase.getLastSegmentNum(periodDurationUs); + public int getSegmentCount(long periodDurationUs) { + return segmentBase.getSegmentCount(periodDurationUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java new file mode 100644 index 0000000000..51451a83c2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.dash.manifest; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Uniquely identifies a {@link Representation} in a {@link DashManifest}. + */ +public final class RepresentationKey implements Parcelable, Comparable { + + public final int periodIndex; + public final int adaptationSetIndex; + public final int representationIndex; + + public RepresentationKey(int periodIndex, int adaptationSetIndex, int representationIndex) { + this.periodIndex = periodIndex; + this.adaptationSetIndex = adaptationSetIndex; + this.representationIndex = representationIndex; + } + + @Override + public String toString() { + return periodIndex + "." + adaptationSetIndex + "." + representationIndex; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(periodIndex); + dest.writeInt(adaptationSetIndex); + dest.writeInt(representationIndex); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public RepresentationKey createFromParcel(Parcel in) { + return new RepresentationKey(in.readInt(), in.readInt(), in.readInt()); + } + + @Override + public RepresentationKey[] newArray(int size) { + return new RepresentationKey[size]; + } + }; + + // Comparable implementation. + + @Override + public int compareTo(RepresentationKey o) { + int result = periodIndex - o.periodIndex; + if (result == 0) { + result = adaptationSetIndex - o.adaptationSetIndex; + if (result == 0) { + result = representationIndex - o.representationIndex; + } + } + return result; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 70a65e932a..4f7dc81fc5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -130,18 +130,22 @@ public abstract class SegmentBase { */ public int getSegmentNum(long timeUs, long periodDurationUs) { final int firstSegmentNum = getFirstSegmentNum(); - int lowIndex = firstSegmentNum; - int highIndex = getLastSegmentNum(periodDurationUs); + final int segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount == 0) { + return firstSegmentNum; + } if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; int segmentNum = startNumber + (int) (timeUs / durationUs); // Ensure we stay within bounds. - return segmentNum < lowIndex ? lowIndex - : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex - : segmentNum; + return segmentNum < firstSegmentNum ? firstSegmentNum + : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum + : Math.min(segmentNum, firstSegmentNum + segmentCount - 1); } else { - // The high index cannot be unbounded. Identify the segment using binary search. + // The index cannot be unbounded. Identify the segment using binary search. + int lowIndex = firstSegmentNum; + int highIndex = firstSegmentNum + segmentCount - 1; while (lowIndex <= highIndex) { int midIndex = lowIndex + (highIndex - lowIndex) / 2; long midTimeUs = getSegmentTimeUs(midIndex); @@ -165,7 +169,9 @@ public abstract class SegmentBase { long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - return sequenceNumber == getLastSegmentNum(periodDurationUs) + int segmentCount = getSegmentCount(periodDurationUs); + return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } @@ -201,9 +207,9 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getLastSegmentNum(long) + * @see DashSegmentIndex#getSegmentCount(long) */ - public abstract int getLastSegmentNum(long periodDurationUs); + public abstract int getSegmentCount(long periodDurationUs); /** * @see DashSegmentIndex#isExplicit() @@ -250,8 +256,8 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return startNumber + mediaSegments.size() - 1; + public int getSegmentCount(long periodDurationUs) { + return mediaSegments.size(); } @Override @@ -322,14 +328,14 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { + public int getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { - return segmentTimeline.size() + startNumber - 1; - } else if (periodDurationUs == C.TIME_UNSET) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } else { + return segmentTimeline.size(); + } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; - return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; + return (int) Util.ceilDivide(periodDurationUs, durationUs); + } else { + return DashSegmentIndex.INDEX_UNBOUNDED; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java index 083046d073..4ce49c5ffe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java @@ -57,8 +57,8 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return 0; + public int getSegmentCount(long periodDurationUs) { + return 1; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java new file mode 100644 index 0000000000..b90dcb2139 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.hls; + +import com.google.android.exoplayer2.upstream.DataSource; + +/** + * Default implementation of {@link HlsDataSourceFactory}. + */ +public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + /** + * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types. + */ + public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public DataSource createDataSource(int dataType) { + return dataSourceFactory.createDataSource(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index c7c66fbd61..ea99dae345 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; +import java.util.List; import java.util.Locale; /** @@ -80,11 +81,13 @@ import java.util.Locale; } - private final DataSource dataSource; + private final DataSource mediaDataSource; + private final DataSource encryptionDataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; private final HlsUrl[] variants; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; + private final List muxedCaptionFormats; private boolean isTimestampMaster; private byte[] scratchSpace; @@ -103,24 +106,28 @@ import java.util.Locale; /** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. - * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the + * chunks. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { + HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, + List muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; - this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; - + this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { variantFormats[i] = variants[i].format; initialTrackSelection[i] = i; } + mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); + encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); } @@ -275,14 +282,14 @@ import java.util.Locale; int discontinuitySequence = mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - discontinuitySequence, startTimeUs); + discontinuitySequence); // Configure the data source and spec for the chunk. Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, selectedUrl, - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, + muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); } @@ -337,7 +344,7 @@ import java.util.Locale; private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); - return new EncryptionKeyChunk(dataSource, dataSpec, variants[variantIndex].format, + return new EncryptionKeyChunk(encryptionDataSource, dataSpec, variants[variantIndex].format, trackSelectionReason, trackSelectionData, scratchSpace, iv); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java new file mode 100644 index 0000000000..30e7af5a0b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.hls; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; + +/** + * Creates {@link DataSource}s for HLS playlists, encryption and media chunks. + */ +public interface HlsDataSourceFactory { + + /** + * Creates a {@link DataSource} for the given data type. + * + * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C} + * {@code .DATA_TYPE_*} constants. + * @return A {@link DataSource} for the given data type. + */ + DataSource createDataSource(int dataType); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java new file mode 100644 index 0000000000..81d63fd4ad --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source.hls; + +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; + +/** + * Holds a master playlist along with a snapshot of one of its media playlists. + */ +public final class HlsManifest { + + /** + * The master playlist of an HLS stream. + */ + public final HlsMasterPlaylist masterPlaylist; + /** + * A snapshot of a media playlist referred to by {@link #masterPlaylist}. + */ + public final HlsMediaPlaylist mediaPlaylist; + + /** + * @param masterPlaylist The master playlist. + * @param mediaPlaylist The media playlist. + */ + HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) { + this.masterPlaylist = masterPlaylist; + this.mediaPlaylist = mediaPlaylist; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 924d3d3ece..6f516923f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.text.TextUtils; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -56,6 +57,7 @@ import java.util.concurrent.atomic.AtomicInteger; private static final String EC3_FILE_EXTENSION = ".ec3"; private static final String MP3_FILE_EXTENSION = ".mp3"; private static final String MP4_FILE_EXTENSION = ".mp4"; + private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -83,6 +85,7 @@ import java.util.concurrent.atomic.AtomicInteger; private final Extractor previousExtractor; private final boolean shouldSpliceIn; private final boolean needNewExtractor; + private final List muxedCaptionFormats; private final boolean isPackedAudio; private final Id3Decoder id3Decoder; @@ -101,6 +104,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. @@ -114,17 +118,19 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs, - long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, - boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, - HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { + HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, + Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, + int discontinuitySequenceNumber, boolean isMasterTimestampSource, + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, + byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; + this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - this.discontinuitySequenceNumber = discontinuitySequenceNumber; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; lastPathSegment = dataSpec.uri.getLastPathSegment(); @@ -236,6 +242,9 @@ import java.util.concurrent.atomic.AtomicInteger; } if (!isMasterTimestampSource) { timestampAdjuster.waitUntilInitialized(); + } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) { + // We're the master and we haven't set the desired first sample timestamp yet. + timestampAdjuster.setFirstSampleTimestampUs(startTimeUs); } try { ExtractorInput input = new DefaultExtractorInput(dataSource, @@ -243,10 +252,8 @@ import java.util.concurrent.atomic.AtomicInteger; if (extractor == null) { // Media segment format is packed audio. long id3Timestamp = peekId3PrivTimestamp(input); - if (id3Timestamp == C.TIME_UNSET) { - throw new ParserException("ID3 PRIV timestamp missing."); - } - extractor = buildPackedAudioExtractor(timestampAdjuster.adjustTsTimestamp(id3Timestamp)); + extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET + ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); } if (skipLoadedBytes) { input.skipFully(bytesLoaded); @@ -341,13 +348,18 @@ import java.util.concurrent.atomic.AtomicInteger; // Only reuse TS and fMP4 extractors. usingNewExtractor = false; extractor = previousExtractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster); } else { // MPEG-2 TS segments, but we need a new extractor. // This flag ensures the change of pid between streams does not affect the sample queues. @DefaultTsPayloadReaderFactory.Flags int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; + if (!muxedCaptionFormats.isEmpty()) { + // The playlist declares closed caption renditions, we should ignore descriptors. + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } String codecs = trackFormat.codecs; if (!TextUtils.isEmpty(codecs)) { // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really @@ -360,8 +372,8 @@ import java.util.concurrent.atomic.AtomicInteger; esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; } } - extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); + extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); } if (usingNewExtractor) { extractor.init(extractorOutput); @@ -377,7 +389,7 @@ import java.util.concurrent.atomic.AtomicInteger; || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { extractor = new Ac3Extractor(startTimeUs); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(startTimeUs); + extractor = new Mp3Extractor(0, startTimeUs); } else { throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 6082372b05..3a833f5468 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -30,10 +30,10 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUr import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; @@ -44,7 +44,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsPlaylistTracker.PlaylistEventListener { private final HlsPlaylistTracker playlistTracker; - private final DataSource.Factory dataSourceFactory; + private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -61,7 +61,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory, + public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, long positionUs) { this.playlistTracker = playlistTracker; @@ -189,6 +189,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -317,7 +322,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); @@ -327,7 +332,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Build audio stream wrappers. for (int i = 0; i < audioRenditions.size(); i++) { sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, null); + new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList()); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -336,20 +341,18 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = 0; i < subtitleRenditions.size(); i++) { HlsUrl url = subtitleRenditions.get(i); sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - null); + Collections.emptyList()); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, Format muxedCaptionFormat) { - DataSource dataSource = dataSourceFactory.createDataSource(); - HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, - timestampAdjusterProvider); + Format muxedAudioFormat, List muxedCaptionFormats) { + HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, + dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, - eventDispatcher); + preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } private void continuePreparingOrLoading() { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 10e12f0ec6..3cd9f19522 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -44,7 +44,7 @@ public final class HlsMediaSource implements MediaSource, public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; private final Uri manifestUri; - private final DataSource.Factory dataSourceFactory; + private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; @@ -60,6 +60,13 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount, + eventHandler, eventListener); + } + + public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; @@ -122,7 +129,8 @@ public final class HlsMediaSource implements MediaSource, timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); } - sourceListener.onSourceInfoRefreshed(timeline, playlist); + sourceListener.onSourceInfoRefreshed(timeline, + new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 04fe8a093c..d8eb7e1ae8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -45,8 +45,8 @@ import java.io.IOException; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return sampleStreamWrapper.readData(group, formatHolder, buffer); + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { + return sampleStreamWrapper.readData(group, formatHolder, buffer, requireFormat); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index a9bbddb69c..8bd966f177 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -77,7 +77,6 @@ import java.util.LinkedList; private final HlsChunkSource chunkSource; private final Allocator allocator; private final Format muxedAudioFormat; - private final Format muxedCaptionFormat; private final int minLoadableRetryCount; private final Loader loader; private final EventDispatcher eventDispatcher; @@ -112,23 +111,19 @@ import java.util.LinkedList; * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. - * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, - * this is the audio {@link Format} as defined by the playlist. - * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed - * captions, this is the audio {@link Format} as defined by the playlist. + * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, - Allocator allocator, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount, + EventDispatcher eventDispatcher) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormat = muxedCaptionFormat; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); @@ -157,7 +152,7 @@ import java.util.LinkedList; * prepare. */ public void prepareSingleTrack(Format format) { - track(0).format(format); + track(0, C.TRACK_TYPE_UNKNOWN).format(format); sampleQueuesBuilt = true; maybeFinishPrepare(); } @@ -183,6 +178,7 @@ import java.util.LinkedList; } } // Enable new tracks. + TrackSelection primaryTrackSelection = null; boolean selectedNewTracks = false; for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { @@ -190,6 +186,7 @@ import java.util.LinkedList; int group = trackGroups.indexOf(selection.getTrackGroup()); setTrackGroupEnabledState(group, true); if (group == primaryTrackGroupIndex) { + primaryTrackSelection = selection; chunkSource.selectTracks(selection); } streams[i] = new HlsSampleStream(this, group); @@ -206,6 +203,14 @@ import java.util.LinkedList; sampleQueues.valueAt(i).disable(); } } + if (primaryTrackSelection != null && !mediaChunks.isEmpty()) { + primaryTrackSelection.updateSelectedTrack(0); + int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { + // The loaded preparation chunk does match the selection. We discard it. + seekTo(lastSeekPositionUs); + } + } } // Cancel requests if necessary. if (enabledTrackCount == 0) { @@ -266,15 +271,6 @@ import java.util.LinkedList; released = true; } - public long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - return largestQueuedTimestampUs; - } - public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } @@ -294,7 +290,8 @@ import java.util.LinkedList; chunkSource.maybeThrowError(); } - /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer) { + /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } @@ -311,8 +308,8 @@ import java.util.LinkedList; } downstreamTrackFormat = trackFormat; - return sampleQueues.valueAt(group).readData(formatHolder, buffer, loadingFinished, - lastSeekPositionUs); + return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, + loadingFinished, lastSeekPositionUs); } /* package */ void skipToKeyframeBefore(int group, long timeUs) { @@ -466,7 +463,7 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id) { + public DefaultTrackOutput track(int id, int type) { if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } @@ -589,14 +586,8 @@ import java.util.LinkedList; trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { - Format trackFormat = null; - if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) { - if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) { - trackFormat = muxedAudioFormat; - } else if (MimeTypes.APPLICATION_CEA608.equals(sampleFormat.sampleMimeType)) { - trackFormat = muxedCaptionFormat; - } - } + Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO + && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java index 41fb2c1512..85a4276ea2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java @@ -36,13 +36,12 @@ public final class TimestampAdjusterProvider { * a chunk with a given discontinuity sequence. * * @param discontinuitySequence The chunk's discontinuity sequence. - * @param startTimeUs The chunk's start time. * @return A {@link TimestampAdjuster}. */ - public TimestampAdjuster getAdjuster(int discontinuitySequence, long startTimeUs) { + public TimestampAdjuster getAdjuster(int discontinuitySequence) { TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence); if (adjuster == null) { - adjuster = new TimestampAdjuster(startTimeUs); + adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET); timestampAdjusters.put(discontinuitySequence, adjuster); } return adjuster; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index c8928ce65d..12ea2c16c7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -167,7 +167,7 @@ import java.util.regex.Pattern; } private TrackOutput buildTrackOutput(long subsampleOffsetUs) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, language, null, subsampleOffsetUs)); output.endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index b7426fd03d..5580017805 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -31,27 +31,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { - public final String name; public final String url; public final Format format; - public final Format videoFormat; - public final Format audioFormat; - public final Format[] textFormats; public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format.NO_VALUE, 0, null); - return new HlsUrl(null, baseUri, format, null, null, null); + return new HlsUrl(baseUri, format); } - public HlsUrl(String name, String url, Format format, Format videoFormat, Format audioFormat, - Format[] textFormats) { - this.name = name; + public HlsUrl(String url, Format format) { this.url = url; this.format = format; - this.videoFormat = videoFormat; - this.audioFormat = audioFormat; - this.textFormats = textFormats; } } @@ -61,22 +52,23 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List subtitles; public final Format muxedAudioFormat; - public final Format muxedCaptionFormat; + public final List muxedCaptionFormats; public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) { + List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { super(baseUri, HlsPlaylist.TYPE_MASTER); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormat = muxedCaptionFormat; + this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); } public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, + Collections.emptyList()); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index b8d8d69af4..9ef28bdb8d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -78,8 +78,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public static final int PLAYLIST_TYPE_VOD = 1; public static final int PLAYLIST_TYPE_EVENT = 2; - @PlaylistType - public final int playlistType; + @PlaylistType public final int playlistType; public final long startOffsetUs; public final long startTimeUs; public final boolean hasDiscontinuitySequence; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index fb62d9978e..aecd2fb324 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -34,8 +34,7 @@ public abstract class HlsPlaylist { public static final int TYPE_MEDIA = 1; public final String baseUri; - @Type - public final int type; + @Type public final int type; protected HlsPlaylist(String baseUri, @Type int type) { this.baseUri = baseUri; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index a211417501..d24264cae6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -81,7 +81,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); Format muxedAudioFormat = null; - Format muxedCaptionFormat = null; + ArrayList muxedCaptionFormats = new ArrayList<>(); String line; while (iterator.hasNext()) { @@ -179,31 +180,37 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser playlistBundles; @@ -105,7 +106,7 @@ public final class HlsPlaylistTracker implements Loader.Callback masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(), initialPlaylistUri, C.DATA_TYPE_MANIFEST, - playlistParser); + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, + C.DATA_TYPE_MANIFEST, playlistParser); initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); } @@ -315,7 +316,7 @@ public final class HlsPlaylistTracker implements Loader.Callback(dataSourceFactory.createDataSource(), + mediaPlaylistLoadable = new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, playlistParser); } @@ -464,7 +467,7 @@ public final class HlsPlaylistTracker implements Loader.Callback(manifest.streamElements[streamElementIndex].type, chunkSource, - this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); + return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, null, + chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } private static TrackGroupArray buildTrackGroups(SsManifest manifest) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 844ffc45e6..1bb877eb59 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -128,8 +128,10 @@ public class SsManifest { */ public static class StreamElement { - private static final String URL_PLACEHOLDER_START_TIME = "{start time}"; - private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}"; + private static final String URL_PLACEHOLDER_START_TIME_1 = "{start time}"; + private static final String URL_PLACEHOLDER_START_TIME_2 = "{start_time}"; + private static final String URL_PLACEHOLDER_BITRATE_1 = "{bitrate}"; + private static final String URL_PLACEHOLDER_BITRATE_2 = "{Bitrate}"; public final int type; public final String subType; @@ -216,9 +218,13 @@ public class SsManifest { Assertions.checkState(formats != null); Assertions.checkState(chunkStartTimes != null); Assertions.checkState(chunkIndex < chunkStartTimes.size()); + String bitrateString = Integer.toString(formats[track].bitrate); + String startTimeString = chunkStartTimes.get(chunkIndex).toString(); String chunkUrl = chunkTemplate - .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(formats[track].bitrate)) - .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); + .replace(URL_PLACEHOLDER_BITRATE_1, bitrateString) + .replace(URL_PLACEHOLDER_BITRATE_2, bitrateString) + .replace(URL_PLACEHOLDER_START_TIME_1, startTimeString) + .replace(URL_PLACEHOLDER_START_TIME_2, startTimeString); return UriUtil.resolveToUri(baseUri, chunkUrl); } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java index b7a75ed679..51f5ad0a64 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -94,8 +94,7 @@ public final class CaptionStyleCompat { *

  • {@link #EDGE_TYPE_DEPRESSED} * */ - @EdgeType - public final int edgeType; + @EdgeType public final int edgeType; /** * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index c4c5a7e4ca..176b8ea815 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import android.graphics.Bitmap; import android.graphics.Color; import android.support.annotation.IntDef; import android.text.Layout.Alignment; @@ -78,7 +79,8 @@ public class Cue { public static final int LINE_TYPE_NUMBER = 1; /** - * The cue text. Note the {@link CharSequence} may be decorated with styling spans. + * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated + * with styling spans. */ public final CharSequence text; @@ -87,6 +89,11 @@ public class Cue { */ public final Alignment textAlignment; + /** + * The cue image, or null if this is a text cue. + */ + public final Bitmap bitmap; + /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of @@ -95,8 +102,8 @@ public class Cue { * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the * fractional vertical position relative to the top of the viewport. */ - public final float line; + /** * The type of the {@link #line} value. *

    @@ -122,9 +129,8 @@ public class Cue { * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first * line is visible at the bottom of the viewport. */ + @LineType public final int lineType; - @LineType - public final int lineType; /** * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. @@ -133,9 +139,8 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box * respectively. */ + @AnchorType public final int lineAnchor; - @AnchorType - public final int lineAnchor; /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}. @@ -154,8 +159,7 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box * respectively. */ - @AnchorType - public final int positionAnchor; + @AnchorType public final int positionAnchor; /** * The size of the cue box in the writing direction specified as a fraction of the viewport size @@ -174,7 +178,27 @@ public class Cue { public final int windowColor; /** - * Constructs a cue whose {@link #textAlignment} is null, whose type parameters are set to + * Creates an image cue. + * + * @param bitmap See {@link #bitmap}. + * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed + * as a fraction of the viewport width. + * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a + * fraction of the viewport height. + * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param width The width of the cue, expressed as a fraction of the viewport width. + */ + public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, + float verticalPosition, @AnchorType int verticalPositionAnchor, float width) { + this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, + horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK); + } + + /** + * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. @@ -184,6 +208,8 @@ public class Cue { } /** + * Creates a text cue. + * * @param text See {@link #text}. * @param textAlignment See {@link #textAlignment}. * @param line See {@link #line}. @@ -200,6 +226,8 @@ public class Cue { } /** + * Creates a text cue. + * * @param text See {@link #text}. * @param textAlignment See {@link #textAlignment}. * @param line See {@link #line}. @@ -214,8 +242,16 @@ public class Cue { public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { + this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, + windowColorSet, windowColor); + } + + private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, + @LineType int lineType, @AnchorType int lineAnchor, float position, + @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; + this.bitmap = bitmap; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index ae3bd309ff..dd25ef8345 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -68,6 +69,8 @@ public abstract class SimpleSubtitleDecoder extends ByteBuffer inputData = inputBuffer.data; Subtitle subtitle = decode(inputData.array(), inputData.limit()); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); + // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). + outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); return null; } catch (SubtitleDecoderException e) { return e; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 8dbde1be5e..a7e05a010a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -189,10 +189,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } // Try and read the next subtitle from the source. - int result = readSource(formatHolder, nextInputBuffer); + int result = readSource(formatHolder, nextInputBuffer, false); if (result == C.RESULT_BUFFER_READ) { - // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]) and queue the buffer. - nextInputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 7324c94288..fe9a5fbc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -364,7 +364,7 @@ public final class Cea608Decoder extends CeaDecoder { } else if (isPreambleAddressCode(cc1, cc2)) { handlePreambleAddressCode(cc1, cc2); } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tab(cc2 - 0x20); + currentCueBuilder.setTab(cc2 - 0x20); } else if (isMiscCode(cc1, cc2)) { handleMiscCode(cc2); } @@ -503,11 +503,14 @@ public final class Cea608Decoder extends CeaDecoder { return; } + int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + // Clear the working memory. resetCueBuilders(); - if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { - // When switching to roll-up or unknown, we also need to clear the caption. + if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP + || captionMode == CC_MODE_UNKNOWN) { + // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. cues = null; } } @@ -646,8 +649,8 @@ public final class Cea608Decoder extends CeaDecoder { this.indent = indent; } - public void tab(int tabs) { - tabOffset += tabs; + public void setTab(int tabs) { + tabOffset = tabs; } public void setPreambleStyle(CharacterStyle style) { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 5ca5ce1270..740fd17013 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -43,13 +43,6 @@ import java.util.List; /** * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). - * - *

    This implementation does not provide full compatibility with the CEA-708 specification. Note - * that only the default pen/text and window/cue colors (i.e. text with - * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK} - * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with - * device accessibility settings; all others will use the colors and opacity specified by the - * caption data. */ public final class Cea708Decoder extends CeaDecoder { @@ -218,7 +211,7 @@ public final class Cea708Decoder extends CeaDecoder { } if (!ccValid) { - finalizeCurrentPacket(); + // This byte-pair isn't valid, ignore it and continue. continue; } @@ -266,7 +259,8 @@ public final class Cea708Decoder extends CeaDecoder { if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " - + currentDtvCcPacket.sequenceNumber + ")"); + + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); + return; } serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); @@ -291,19 +285,26 @@ public final class Cea708Decoder extends CeaDecoder { return; } + // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after + // processing the service block any text has been added to the buffer. See CEA-708-B Section + // 8.10.4 for more details. + boolean cuesNeedUpdate = false; + while (serviceBlockPacket.bitsLeft() > 0) { int command = serviceBlockPacket.readBits(8); if (command != COMMAND_EXT1) { if (command <= GROUP_C0_END) { handleC0Command(command); + // If the C0 command was an ETX command, the cues are updated in handleC0Command. } else if (command <= GROUP_G0_END) { handleG0Character(command); + cuesNeedUpdate = true; } else if (command <= GROUP_C1_END) { handleC1Command(command); - // Cues are always updated after a C1 command - cues = getDisplayCues(); + cuesNeedUpdate = true; } else if (command <= GROUP_G1_END) { handleG1Character(command); + cuesNeedUpdate = true; } else { Log.w(TAG, "Invalid base command: " + command); } @@ -314,15 +315,21 @@ public final class Cea708Decoder extends CeaDecoder { handleC2Command(command); } else if (command <= GROUP_G2_END) { handleG2Character(command); + cuesNeedUpdate = true; } else if (command <= GROUP_C3_END) { handleC3Command(command); } else if (command <= GROUP_G3_END) { handleG3Character(command); + cuesNeedUpdate = true; } else { Log.w(TAG, "Invalid extended command: " + command); } } } + + if (cuesNeedUpdate) { + cues = getDisplayCues(); + } } private void handleC0Command(int command) { @@ -463,6 +470,11 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF7: window = (command - COMMAND_DF0); handleDefineWindow(window); + // We also set the current window to the newly defined window. + if (currentWindow != window) { + currentWindow = window; + currentCueBuilder = cueBuilders[window]; + } break; default: Log.w(TAG, "Invalid C1 command: " + command); @@ -864,6 +876,7 @@ public final class Cea708Decoder extends CeaDecoder { private int foregroundColor; private int backgroundColorStartPosition; private int backgroundColor; + private int row; public CueBuilder() { rolledUpCaptions = new LinkedList<>(); @@ -903,6 +916,7 @@ public final class Cea708Decoder extends CeaDecoder { underlineStartPosition = C.POSITION_UNSET; foregroundColorStartPosition = C.POSITION_UNSET; backgroundColorStartPosition = C.POSITION_UNSET; + row = 0; } public boolean isDefined() { @@ -1037,7 +1051,16 @@ public final class Cea708Decoder extends CeaDecoder { } public void setPenLocation(int row, int column) { - // TODO: Support moving the pen location with a window. + // TODO: Support moving the pen location with a window properly. + + // Until we support proper pen locations, if we encounter a row that's different from the + // previous one, we should append a new line. Otherwise, we'll see strings that should be + // on new lines concatenated with the previous, resulting in 2 words being combined, as + // well as potentially drawing beyond the width of the window/screen. + if (this.row != row) { + append('\n'); + } + this.row = row; } public void backspace() { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index f479050d57..fac0982e65 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -75,7 +75,13 @@ import java.util.TreeSet; public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.add(inputBuffer); + if (inputBuffer.isDecodeOnly()) { + // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow + // for decoding to begin mid-stream. + releaseInputBuffer(inputBuffer); + } else { + queuedInputBuffers.add(inputBuffer); + } dequeuedInputBuffer = null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index 3053debfcf..130c7461f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.cea; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -24,6 +25,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public final class CeaUtil { + private static final String TAG = "CeaUtil"; + private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; private static final int PROVIDER_CODE = 0x31; @@ -32,30 +35,23 @@ public final class CeaUtil { /** * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages - * as samples to the provided output. + * as samples to all of the provided outputs. * * @param presentationTimeUs The presentation time in microseconds for any samples. * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type. - * @param output The output to which any samples should be written. + * @param outputs The outputs to which any samples should be written. */ public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, - TrackOutput output) { - int b; + TrackOutput[] outputs) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { - // Parse payload type. - int payloadType = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadType += b; - } while (b == 0xFF); - // Parse payload size. - int payloadSize = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadSize += b; - } while (b == 0xFF); + int payloadType = readNon255TerminatedValue(seiBuffer); + int payloadSize = readNon255TerminatedValue(seiBuffer); // Process the payload. - if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { + if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { + // This might occur if we're trying to read an encrypted SEI NAL unit. + Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); + seiBuffer.setPosition(seiBuffer.limit()); + } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { // Ignore country_code (1) + provider_code (2) + user_identifier (4) // + user_data_type_code (1). seiBuffer.skipBytes(8); @@ -66,8 +62,12 @@ public final class CeaUtil { // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) // + cc_data_1 (8) + cc_data_2 (8). int sampleLength = ccCount * 3; - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + int sampleStartPosition = seiBuffer.getPosition(); + for (TrackOutput output : outputs) { + seiBuffer.setPosition(sampleStartPosition); + output.sampleData(seiBuffer, sampleLength); + output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + } // Ignore trailing information in SEI, if any. seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); } else { @@ -76,6 +76,27 @@ public final class CeaUtil { } } + /** + * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a + * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the + * number of 0xFF bytes and T is the value of the terminating byte. + * + * @param buffer The buffer from which to read the value. + * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + */ + private static int readNon255TerminatedValue(ParsableByteArray buffer) { + int b; + int value = 0; + do { + if (buffer.bytesLeft() == 0) { + return -1; + } + b = buffer.readUnsignedByte(); + value += b; + } while (b == 0xFF); + return value; + } + /** * Inspects an sei message to determine whether it contains CEA-608. *

    diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index e4c36be03a..90f93d5b21 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -56,16 +56,11 @@ import java.lang.annotation.RetentionPolicy; private boolean hasFontColor; private int backgroundColor; private boolean hasBackgroundColor; - @OptionalBoolean - private int linethrough; - @OptionalBoolean - private int underline; - @OptionalBoolean - private int bold; - @OptionalBoolean - private int italic; - @FontSizeUnit - private int fontSizeUnit; + @OptionalBoolean private int linethrough; + @OptionalBoolean private int underline; + @OptionalBoolean private int bold; + @OptionalBoolean private int italic; + @FontSizeUnit private int fontSizeUnit; private float fontSize; private String id; private TtmlStyle inheritableStyle; @@ -85,8 +80,7 @@ import java.lang.annotation.RetentionPolicy; * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} * or {@link #STYLE_BOLD_ITALIC}. */ - @StyleFlags - public int getStyle() { + @StyleFlags public int getStyle() { if (bold == UNSPECIFIED && italic == UNSPECIFIED) { return UNSPECIFIED; } @@ -255,8 +249,7 @@ import java.lang.annotation.RetentionPolicy; return this; } - @FontSizeUnit - public int getFontSizeUnit() { + @FontSizeUnit public int getFontSizeUnit() { return fontSizeUnit; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 157174a8f0..10c17e2888 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -69,16 +69,11 @@ import java.util.List; private boolean hasFontColor; private int backgroundColor; private boolean hasBackgroundColor; - @OptionalBoolean - private int linethrough; - @OptionalBoolean - private int underline; - @OptionalBoolean - private int bold; - @OptionalBoolean - private int italic; - @FontSizeUnit - private int fontSizeUnit; + @OptionalBoolean private int linethrough; + @OptionalBoolean private int underline; + @OptionalBoolean private int bold; + @OptionalBoolean private int italic; + @FontSizeUnit private int fontSizeUnit; private float fontSize; private Layout.Alignment textAlign; @@ -162,8 +157,7 @@ import java.util.List; * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} * or {@link #STYLE_BOLD_ITALIC}. */ - @StyleFlags - public int getStyle() { + @StyleFlags public int getStyle() { if (bold == UNSPECIFIED && italic == UNSPECIFIED) { return UNSPECIFIED; } @@ -260,8 +254,7 @@ import java.util.List; return this; } - @FontSizeUnit - public int getFontSizeUnit() { + @FontSizeUnit public int getFontSizeUnit() { return fontSizeUnit; } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java similarity index 88% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java rename to library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 868303cc5b..dc78e28e56 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -24,13 +24,13 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.List; /** - * A bandwidth based adaptive {@link TrackSelection} for video, whose selected track is updated to - * be the one of highest quality given the current network conditions and the state of the buffer. + * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one + * of highest quality given the current network conditions and the state of the buffer. */ -public class AdaptiveVideoTrackSelection extends BaseTrackSelection { +public class AdaptiveTrackSelection extends BaseTrackSelection { /** - * Factory for {@link AdaptiveVideoTrackSelection} instances. + * Factory for {@link AdaptiveTrackSelection} instances. */ public static final class Factory implements TrackSelection.Factory { @@ -79,8 +79,8 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } @Override - public AdaptiveVideoTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new AdaptiveVideoTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, + public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, bandwidthFraction); } @@ -104,12 +104,12 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { private int reason; /** - * @param group The {@link TrackGroup}. Must not be null. + * @param group The {@link TrackGroup}. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. + * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ - public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) { this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, @@ -118,9 +118,9 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } /** - * @param group The {@link TrackGroup}. Must not be null. + * @param group The {@link TrackGroup}. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. + * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a * bandwidth estimate is unavailable. @@ -136,7 +136,7 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. */ - public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, float bandwidthFraction) { @@ -208,15 +208,18 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); Format idealFormat = getFormat(idealSelectedIndex); - // Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution and - // bitrate are both lower than the ideal track. + // If the chunks contain video, discard from the first SD chunk beyond + // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal + // track. for (int i = 0; i < queueSize; i++) { MediaChunk chunk = queue.get(i); + Format format = chunk.trackFormat; long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs - && chunk.trackFormat.bitrate < idealFormat.bitrate - && chunk.trackFormat.height < idealFormat.height - && chunk.trackFormat.height < 720 && chunk.trackFormat.width < 1280) { + && format.bitrate < idealFormat.bitrate + && format.height != Format.NO_VALUE && format.height < 720 + && format.width != Format.NO_VALUE && format.width < 1280 + && format.height < idealFormat.height) { return i; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index c81ffb441f..054ee7973f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -148,7 +148,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } /** - * Returns whether the track at the specified index in the selection is blaclisted. + * Returns whether the track at the specified index in the selection is blacklisted. * * @param index The index of the track in the selection. * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}. diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f62d5d9075..1fa372ca0a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -54,6 +54,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean allowNonSeamlessAdaptiveness; public final int maxVideoWidth; public final int maxVideoHeight; + public final int maxVideoBitrate; public final boolean exceedVideoConstraintsIfNecessary; public final boolean exceedRendererCapabilitiesIfNecessary; public final int viewportWidth; @@ -68,14 +69,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

  • Adaptation between different mime types is not allowed.
  • *
  • Non seamless adaptation is allowed.
  • *
  • No max limit for video width/height.
  • + *
  • No max video bitrate.
  • *
  • Video constraints are exceeded if no supported selection can be made otherwise.
  • *
  • Renderer capabilities are exceeded if no supported selection can be made.
  • *
  • No viewport width/height constraints are set.
  • * */ public Parameters() { - this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true, true, - Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, + true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** @@ -88,6 +90,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. * @param maxVideoWidth Maximum allowed video width. * @param maxVideoHeight Maximum allowed video height. + * @param maxVideoBitrate Maximum allowed video bitrate. * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no * selection can be made otherwise. * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no @@ -98,15 +101,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, - int maxVideoWidth, int maxVideoHeight, boolean exceedVideoConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, + boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, + int viewportWidth, int viewportHeight, boolean orientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; this.maxVideoWidth = maxVideoWidth; this.maxVideoHeight = maxVideoHeight; + this.maxVideoBitrate = maxVideoBitrate; this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.viewportWidth = viewportWidth; @@ -130,8 +134,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -148,8 +152,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -164,8 +168,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -180,8 +184,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -197,8 +201,24 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); + } + + /** + * Returns a {@link Parameters} instance with the provided max video bitrate. + * + * @param maxVideoBitrate The max video bitrate. + * @return A {@link Parameters} instance with the provided max video bitrate. + */ + public Parameters withMaxVideoBitrate(int maxVideoBitrate) { + if (maxVideoBitrate == this.maxVideoBitrate) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -235,8 +255,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -255,8 +275,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -275,8 +295,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -319,6 +339,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && orientationMayChange == other.orientationMayChange && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight + && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); } @@ -331,6 +352,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; + result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (orientationMayChange ? 1 : 0); @@ -399,18 +421,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) throws ExoPlaybackException { // Make a track selection for each renderer. - TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCapabilities.length]; + int rendererCount = rendererCapabilities.length; + TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; Parameters params = paramsReference.get(); - for (int i = 0; i < rendererCapabilities.length; i++) { + + for (int i = 0; i < rendererCount; i++) { + if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { + rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], + rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, + params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, + params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, + params.orientationMayChange, adaptiveVideoTrackSelectionFactory, + params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); + } + } + + for (int i = 0; i < rendererCount; i++) { switch (rendererCapabilities[i].getTrackType()) { case C.TRACK_TYPE_VIDEO: - rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveVideoTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, - params.exceedRendererCapabilitiesIfNecessary); + // Already done. Do nothing. break; case C.TRACK_TYPE_AUDIO: rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], @@ -436,30 +465,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange, + int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, + int viewportWidth, int viewportHeight, boolean orientationMayChange, TrackSelection.Factory adaptiveVideoTrackSelectionFactory, boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { TrackSelection selection = null; if (adaptiveVideoTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - maxVideoWidth, maxVideoHeight, allowNonSeamlessAdaptiveness, + maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, orientationMayChange, adaptiveVideoTrackSelectionFactory); } if (selection == null) { selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange, exceedConstraintsIfNecessary, - exceedRendererCapabilitiesIfNecessary); + maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange, + exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary); } return selection; } private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange, + int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, + int viewportWidth, int viewportHeight, boolean orientationMayChange, TrackSelection.Factory adaptiveVideoTrackSelectionFactory) throws ExoPlaybackException { int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) @@ -470,7 +499,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup group = groups.get(i); int[] adaptiveTracks = getAdaptiveTracksForGroup(group, formatSupport[i], allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange); + maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); if (adaptiveTracks.length > 0) { return adaptiveVideoTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); } @@ -480,7 +509,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveTracksForGroup(TrackGroup group, int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, - int maxVideoHeight, int viewportWidth, int viewportHeight, boolean orientationMayChange) { + int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, + boolean orientationMayChange) { if (group.length < 2) { return NO_TRACKS; } @@ -499,11 +529,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); String sampleMimeType = group.getFormat(trackIndex).sampleMimeType; - if (!seenMimeTypes.contains(sampleMimeType)) { - seenMimeTypes.add(sampleMimeType); + if (seenMimeTypes.add(sampleMimeType)) { int countForMimeType = getAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight, - selectedTrackIndices); + maxVideoBitrate, selectedTrackIndices); if (countForMimeType > selectedMimeTypeTrackCount) { selectedMimeType = sampleMimeType; selectedMimeTypeTrackCount = countForMimeType; @@ -514,19 +543,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Filter by the selected mime type. filterAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, - selectedMimeType, maxVideoWidth, maxVideoHeight, selectedTrackIndices); + selectedMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices); return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } private static int getAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - List selectedTrackIndices) { + int maxVideoBitrate, List selectedTrackIndices) { int adaptiveTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); if (isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight)) { + formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + maxVideoBitrate)) { adaptiveTrackCount++; } } @@ -535,28 +565,31 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void filterAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - List selectedTrackIndices) { + int maxVideoBitrate, List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight)) { + formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + maxVideoBitrate)) { selectedTrackIndices.remove(i); } } } private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, - int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight) { + int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, + int maxVideoBitrate) { return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); + && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int viewportWidth, - int viewportHeight, boolean orientationMayChange, boolean exceedConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary) { + int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, + int viewportWidth, int viewportHeight, boolean orientationMayChange, + boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -572,7 +605,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); + && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); if (!isWithinConstraints && !exceedConstraintsIfNecessary) { // Track should not be selected. continue; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 04f3b986bd..d4f09b1721 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -18,11 +18,13 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; 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.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.StaticLayout; @@ -65,6 +67,7 @@ import com.google.android.exoplayer2.util.Util; // Previous input variables. private CharSequence cueText; private Alignment cueTextAlignment; + private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @@ -93,6 +96,7 @@ import com.google.android.exoplayer2.util.Util; private int textLeft; private int textTop; private int textPaddingX; + private Rect bitmapRect; @SuppressWarnings("ResourceType") public SubtitlePainter(Context context) { @@ -141,21 +145,28 @@ import com.google.android.exoplayer2.util.Util; public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { - CharSequence cueText = cue.text; - if (TextUtils.isEmpty(cueText)) { - // Nothing to draw. - return; - } - - int windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; - - if (!applyEmbeddedStyles) { - // Strip out any embedded styling. - cueText = cueText.toString(); - windowColor = style.windowColor; + boolean isTextCue = cue.bitmap == null; + CharSequence cueText = null; + Bitmap cueBitmap = null; + int windowColor = Color.BLACK; + if (isTextCue) { + cueText = cue.text; + if (TextUtils.isEmpty(cueText)) { + // Nothing to draw. + return; + } + windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; + if (!applyEmbeddedStyles) { + // Strip out any embedded styling. + cueText = cueText.toString(); + windowColor = style.windowColor; + } + } else { + cueBitmap = cue.bitmap; } if (areCharSequencesEqual(this.cueText, cueText) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) + && this.cueBitmap == cueBitmap && this.cueLine == cue.line && this.cueLineType == cue.lineType && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) @@ -176,12 +187,13 @@ import com.google.android.exoplayer2.util.Util; && this.parentRight == cueBoxRight && this.parentBottom == cueBoxBottom) { // We can use the cached layout. - drawLayout(canvas); + drawLayout(canvas, isTextCue); return; } this.cueText = cueText; this.cueTextAlignment = cue.textAlignment; + this.cueBitmap = cueBitmap; this.cueLine = cue.line; this.cueLineType = cue.lineType; this.cueLineAnchor = cue.lineAnchor; @@ -202,6 +214,15 @@ import com.google.android.exoplayer2.util.Util; this.parentRight = cueBoxRight; this.parentBottom = cueBoxBottom; + if (isTextCue) { + setupTextLayout(); + } else { + setupBitmapLayout(); + } + drawLayout(canvas, isTextCue); + } + + private void setupTextLayout() { int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -237,7 +258,7 @@ import com.google.android.exoplayer2.util.Util; int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft; textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2 - : anchorPosition; + : anchorPosition; textLeft = Math.max(textLeft, parentLeft); textRight = Math.min(textLeft + textWidth, parentRight); } else { @@ -245,6 +266,12 @@ import com.google.android.exoplayer2.util.Util; textRight = textLeft + textWidth; } + textWidth = textRight - textLeft; + if (textWidth <= 0) { + Log.w(TAG, "Skipped drawing subtitle cue (invalid horizontal positioning)"); + return; + } + int textTop; if (cueLine != Cue.DIMEN_UNSET) { int anchorPosition; @@ -261,7 +288,7 @@ import com.google.android.exoplayer2.util.Util; } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2 - : anchorPosition; + : anchorPosition; if (textTop + textHeight > parentBottom) { textTop = parentBottom - textHeight; } else if (textTop < parentTop) { @@ -271,25 +298,38 @@ import com.google.android.exoplayer2.util.Util; textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction); } - textWidth = textRight - textLeft; - // Update the derived drawing variables. this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; - - drawLayout(canvas); } - /** - * Draws {@link #textLayout} into the provided canvas. - * - * @param canvas The canvas into which to draw. - */ - private void drawLayout(Canvas canvas) { - final StaticLayout layout = textLayout; + private void setupBitmapLayout() { + int parentWidth = parentRight - parentLeft; + int parentHeight = parentBottom - parentTop; + float anchorX = parentLeft + (parentWidth * cuePosition); + float anchorY = parentTop + (parentHeight * cueLine); + int width = Math.round(parentWidth * cueSize); + int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) + : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); + int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); + bitmapRect = new Rect(x, y, x + width, y + height); + } + + private void drawLayout(Canvas canvas, boolean isTextCue) { + if (isTextCue) { + drawTextLayout(canvas); + } else { + drawBitmapLayout(canvas); + } + } + + private void drawTextLayout(Canvas canvas) { + StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. return; @@ -347,6 +387,10 @@ import com.google.android.exoplayer2.util.Util; canvas.restoreToCount(saveCount); } + private void drawBitmapLayout(Canvas canvas) { + canvas.drawBitmap(cueBitmap, null, bitmapRect, null); + } + /** * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the * latter only checks the text of each sequence, and does not check for equality of styling that diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 133e71f6e2..d3c63b4454 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -47,7 +47,9 @@ public final class DataSpec { */ public static final int FLAG_ALLOW_GZIP = 1 << 0; - /** Permits content to be cached even if its length can not be resolved. */ + /** + * Permits content to be cached even if its length can not be resolved. + */ public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; /** @@ -82,8 +84,7 @@ public final class DataSpec { * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. */ - @Flags - public final int flags; + @Flags public final int flags; /** * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index ae6f1e0691..9d13383a56 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -81,7 +81,7 @@ public final class DefaultDataSource implements DataSource { boolean allowCrossProtocolRedirects) { this(context, listener, new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects)); + readTimeoutMillis, allowCrossProtocolRedirects, null)); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index ca0fda9399..599cdddeb9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -32,7 +32,6 @@ import java.net.HttpURLConnection; import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URL; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -44,8 +43,8 @@ import java.util.regex.Pattern; *

    * By default this implementation will not follow cross-protocol redirects (i.e. redirects from * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the - * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)} - * constructor and passing {@code true} as the final argument. + * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, + * RequestProperties)} constructor and passing {@code true} as the second last argument. */ public class DefaultHttpDataSource implements HttpDataSource { @@ -70,7 +69,8 @@ public class DefaultHttpDataSource implements HttpDataSource { private final int readTimeoutMillis; private final String userAgent; private final Predicate contentTypePredicate; - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private final TransferListener listener; private DataSpec dataSpec; @@ -121,7 +121,8 @@ public class DefaultHttpDataSource implements HttpDataSource { public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false); + this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, + null); } /** @@ -137,17 +138,21 @@ public class DefaultHttpDataSource implements HttpDataSource { * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled. + * @param defaultRequestProperties The default request properties to be sent to the server as + * HTTP headers or {@code null} if not required. */ public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects) { + int readTimeoutMillis, boolean allowCrossProtocolRedirects, + RequestProperties defaultRequestProperties) { this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; this.listener = listener; - this.requestProperties = new HashMap<>(); + this.requestProperties = new RequestProperties(); this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; } @Override @@ -164,24 +169,18 @@ public class DefaultHttpDataSource implements HttpDataSource { public void setRequestProperty(String name, String value) { Assertions.checkNotNull(name); Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -394,11 +393,14 @@ public class DefaultHttpDataSource implements HttpDataSource { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { + if (defaultRequestProperties != null) { + for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } } + for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + connection.setRequestProperty(property.getKey(), property.getValue()); + } if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 615eb4df97..3b3a5a1c16 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -76,9 +76,10 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { } @Override - protected DefaultHttpDataSource createDataSourceInternal() { + protected DefaultHttpDataSource createDataSourceInternal( + HttpDataSource.RequestProperties defaultRequestProperties) { return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects); + readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 8df8624102..3725fc0052 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -17,12 +17,12 @@ package com.google.android.exoplayer2.upstream; import android.support.annotation.IntDef; import android.text.TextUtils; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,84 +41,173 @@ public interface HttpDataSource extends DataSource { HttpDataSource createDataSource(); /** - * Sets a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Gets the default request properties used by all {@link HttpDataSource}s created by the + * factory. Changes to the properties will be reflected in any future requests made by + * {@link HttpDataSource}s created by the factory. * + * @return The default request properties of the factory. + */ + RequestProperties getDefaultRequestProperties(); + + /** + * Sets a default request header for {@link HttpDataSource} instances created by the factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. * @param name The name of the header field. * @param value The value of the field. */ + @Deprecated void setDefaultRequestProperty(String name, String value); /** - * Clears a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Clears a default request header for {@link HttpDataSource} instances created by the factory. * + * @deprecated Use {@link #getDefaultRequestProperties} instead. * @param name The name of the header field. */ + @Deprecated void clearDefaultRequestProperty(String name); /** - * Clears all default request header fields for all {@link HttpDataSource} instances - * subsequently created by the factory. Previously created instances are not affected. + * Clears all default request headers for all {@link HttpDataSource} instances created by the + * factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. */ + @Deprecated void clearAllDefaultRequestProperties(); } + /** + * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers + * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or + * unintended state. + */ + final class RequestProperties { + + private final Map requestProperties; + private Map requestPropertiesSnapshot; + + public RequestProperties() { + requestProperties = new HashMap<>(); + } + + /** + * Sets the specified property {@code value} for the specified {@code name}. If a property for + * this name previously existed, the old value is replaced by the specified value. + * + * @param name The name of the request property. + * @param value The value of the request property. + */ + public synchronized void set(String name, String value) { + requestPropertiesSnapshot = null; + requestProperties.put(name, value); + } + + /** + * Sets the keys and values contained in the map. If a property previously existed, the old + * value is replaced by the specified value. If a property previously existed and is not in the + * map, the property is left unchanged. + * + * @param properties The request properties. + */ + public synchronized void set(Map properties) { + requestPropertiesSnapshot = null; + requestProperties.putAll(properties); + } + + /** + * Removes all properties previously existing and sets the keys and values of the map. + * + * @param properties The request properties. + */ + public synchronized void clearAndSet(Map properties) { + requestPropertiesSnapshot = null; + requestProperties.clear(); + requestProperties.putAll(properties); + } + + /** + * Removes a request property by name. + * + * @param name The name of the request property to remove. + */ + public synchronized void remove(String name) { + requestPropertiesSnapshot = null; + requestProperties.remove(name); + } + + /** + * Clears all request properties. + */ + public synchronized void clear() { + requestPropertiesSnapshot = null; + requestProperties.clear(); + } + + /** + * Gets a snapshot of the request properties. + * + * @return A snapshot of the request properties. + */ + public synchronized Map getSnapshot() { + if (requestPropertiesSnapshot == null) { + requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties)); + } + return requestPropertiesSnapshot; + } + + } + /** * Base implementation of {@link Factory} that sets default request properties. */ abstract class BaseFactory implements Factory { - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; public BaseFactory() { - requestProperties = new HashMap<>(); + defaultRequestProperties = new RequestProperties(); } @Override public final HttpDataSource createDataSource() { - HttpDataSource dataSource = createDataSourceInternal(); - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { - dataSource.setRequestProperty(property.getKey(), property.getValue()); - } - } - return dataSource; + return createDataSourceInternal(defaultRequestProperties); } + @Override + public final RequestProperties getDefaultRequestProperties() { + return defaultRequestProperties; + } + + @Deprecated @Override public final void setDefaultRequestProperty(String name, String value) { - Assertions.checkNotNull(name); - Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + defaultRequestProperties.set(name, value); } + @Deprecated @Override public final void clearDefaultRequestProperty(String name) { - Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + defaultRequestProperties.remove(name); } + @Deprecated @Override public final void clearAllDefaultRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + defaultRequestProperties.clear(); } /** - * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance without - * default request properties set. Default request properties will be set by - * {@link #createDataSource()} before the instance is returned. + * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance. * - * @return A {@link HttpDataSource} instance without default request properties set. + * @param defaultRequestProperties The default {@code RequestProperties} to be used by the + * {@link HttpDataSource} instance. + * @return A {@link HttpDataSource} instance. */ - protected abstract HttpDataSource createDataSourceInternal(); + protected abstract HttpDataSource createDataSourceInternal(RequestProperties + defaultRequestProperties); } @@ -149,8 +238,7 @@ public interface HttpDataSource extends DataSource { public static final int TYPE_READ = 2; public static final int TYPE_CLOSE = 3; - @Type - public final int type; + @Type public final int type; /** * The {@link DataSpec} associated with the current connection. @@ -232,7 +320,7 @@ public interface HttpDataSource extends DataSource { int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException; /** - * Sets the value of a request header field. The value will be used for subsequent connections + * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * * @param name The name of the header field. @@ -241,7 +329,7 @@ public interface HttpDataSource extends DataSource { void setRequestProperty(String name, String value); /** - * Clears the value of a request header field. The change will apply to subsequent connections + * Clears the value of a request header. The change will apply to subsequent connections * established by the source. * * @param name The name of the header field. @@ -249,7 +337,7 @@ public interface HttpDataSource extends DataSource { void clearRequestProperty(String name); /** - * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. + * Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ void clearAllRequestProperties(); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 64836dae4c..bca90ddc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -199,7 +199,7 @@ public final class Loader implements LoaderErrorThrower { currentTask.cancel(true); } if (postLoadAction != null) { - downloadExecutorService.submit(postLoadAction); + downloadExecutorService.execute(postLoadAction); } downloadExecutorService.shutdown(); } @@ -260,7 +260,7 @@ public final class Loader implements LoaderErrorThrower { if (delayMillis > 0) { sendEmptyMessageDelayed(MSG_START, delayMillis); } else { - submitToExecutor(); + execute(); } } @@ -334,7 +334,7 @@ public final class Loader implements LoaderErrorThrower { return; } if (msg.what == MSG_START) { - submitToExecutor(); + execute(); return; } if (msg.what == MSG_FATAL_ERROR) { @@ -367,9 +367,9 @@ public final class Loader implements LoaderErrorThrower { } } - private void submitToExecutor() { + private void execute() { currentError = null; - downloadExecutorService.submit(currentTask); + downloadExecutorService.execute(currentTask); } private void finish() { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java new file mode 100644 index 0000000000..daad41a9a6 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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.exoplayer2.upstream; + +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.util.PriorityTaskManager; + +/** + * A {@link DataSource.Factory} that produces {@link PriorityDataSource} instances. + */ +public final class PriorityDataSourceFactory implements Factory { + + private final Factory upstreamFactory; + private final PriorityTaskManager priorityTaskManager; + private final int priority; + + /** + * @param upstreamFactory A {@link DataSource.Factory} to be used to create an upstream {@link + * DataSource} for {@link PriorityDataSource}. + * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered. + * @param priority The priority of PriorityDataSource task. + */ + public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager, + int priority) { + this.upstreamFactory = upstreamFactory; + this.priorityTaskManager = priorityTaskManager; + this.priority = priority; + } + + @Override + public PriorityDataSource createDataSource() { + return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager, + priority); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 8dcfe75670..86ff810142 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -198,6 +198,18 @@ public interface Cache { */ boolean isCached(String key, long position, long length); + /** + * Returns the length of the cached data block starting from the {@code position} to the block end + * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap + * to the next cached data up to {@code length} bytes) is returned. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The maximum length of the data to be returned. + * @return the length of the cached or not cached data block length. + */ + long getCachedBytes(String key, long position, long length); + /** * Sets the content length for the given key. * diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 71397bd403..33b1ca58b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -32,6 +32,9 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { + /** Default buffer size. */ + public static final int DEFAULT_BUFFER_SIZE = 20480; + private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; @@ -56,13 +59,15 @@ public final class CacheDataSink implements DataSink { } /** + * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * * @param cache The cache into which data should be written. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, 0); + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 0c8c006e2c..0b9ab66508 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -24,18 +24,27 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; private final long maxCacheFileSize; + private final int bufferSize; /** * @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + } + + /** + * @see CacheDataSink#CacheDataSink(Cache, long, int) + */ + public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { this.cache = cache; this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize); + return new CacheDataSink(cache, maxCacheFileSize, bufferSize); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 9b29984d06..a2e4382e0c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -89,7 +90,7 @@ public final class CacheDataSource implements DataSource { private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; - private final EventListener eventListener; + @Nullable private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; @@ -142,13 +143,14 @@ public final class CacheDataSource implements DataSource { * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. - * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, - DataSink cacheWriteDataSink, @Flags int flags, EventListener eventListener) { + DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; @@ -283,7 +285,6 @@ public final class CacheDataSource implements DataSource { currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. - lockedSpan = span; long length; if (span.isOpenEnded()) { length = bytesRemaining; @@ -294,8 +295,13 @@ public final class CacheDataSource implements DataSource { } } dataSpec = new DataSpec(uri, readPosition, length, key, flags); - currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource - : upstreamDataSource; + if (cacheWriteDataSource != null) { + currentDataSource = cacheWriteDataSource; + lockedSpan = span; + } else { + currentDataSource = upstreamDataSource; + cache.releaseHoleSpan(span); + } } currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index 125bec5fdc..b6fa3b4e2c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -65,10 +65,11 @@ public final class CacheDataSourceFactory implements DataSource.Factory { } @Override - public DataSource createDataSource() { + public CacheDataSource createDataSource() { return new CacheDataSource(cache, upstreamFactory.createDataSource(), cacheReadDataSourceFactory.createDataSource(), - cacheWriteDataSinkFactory.createDataSink(), flags, eventListener); + cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null, + flags, eventListener); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index c744a176ad..fb59d23666 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -106,43 +106,49 @@ import java.util.TreeSet; * which defines the maximum extents of the hole in the cache. */ public SimpleCacheSpan getSpan(long position) { - SimpleCacheSpan span = getSpanInternal(position); - if (!span.isCached) { - SimpleCacheSpan ceilEntry = cachedSpans.ceiling(span); - return ceilEntry == null ? SimpleCacheSpan.createOpenHole(key, position) - : SimpleCacheSpan.createClosedHole(key, position, ceilEntry.position - position); + SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); + SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); + if (floorSpan != null && floorSpan.position + floorSpan.length > position) { + return floorSpan; } - return span; + SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan); + return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position) + : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position); } - /** Queries if a range is entirely available in the cache. */ - public boolean isCached(long position, long length) { - SimpleCacheSpan floorSpan = getSpanInternal(position); - if (!floorSpan.isCached) { + /** + * Returns the length of the cached data block starting from the {@code position} to the block end + * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap + * to the next cached data up to {@code length} bytes) is returned. + * + * @param position The starting position of the data. + * @param length The maximum length of the data to be returned. + * @return the length of the cached or not cached data block length. + */ + public long getCachedBytes(long position, long length) { + SimpleCacheSpan span = getSpan(position); + if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. - return false; + return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length); } long queryEndPosition = position + length; - long currentEndPosition = floorSpan.position + floorSpan.length; - if (currentEndPosition >= queryEndPosition) { - // floorSpan covers the queried region. - return true; - } - for (SimpleCacheSpan next : cachedSpans.tailSet(floorSpan, false)) { - if (next.position > currentEndPosition) { - // There's a hole in the cache within the queried region. - return false; - } - // We expect currentEndPosition to always equal (next.position + next.length), but - // perform a max check anyway to guard against the existence of overlapping spans. - currentEndPosition = Math.max(currentEndPosition, next.position + next.length); - if (currentEndPosition >= queryEndPosition) { - // We've found spans covering the queried region. - return true; + long currentEndPosition = span.position + span.length; + if (currentEndPosition < queryEndPosition) { + for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) { + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + break; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + break; + } } } - // We ran out of spans before covering the queried region. - return false; + return Math.min(currentEndPosition - position, length); } /** @@ -190,15 +196,4 @@ import java.util.TreeSet; return result; } - /** - * Returns the span containing the position. If there isn't one, it returns the lookup span it - * used for searching. - */ - private SimpleCacheSpan getSpanInternal(long position) { - SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); - SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); - return floorSpan == null || floorSpan.position + floorSpan.length <= position ? lookupSpan - : floorSpan; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index e3e887c6ed..14f006c850 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -354,7 +354,13 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.isCached(position, length); + return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length; + } + + @Override + public synchronized long getCachedBytes(String key, long position, long length) { + CachedContent cachedContent = index.get(key); + return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index a452871afc..ab2fec0db7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -103,7 +103,9 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39; private static final Object scratchEscapePositionsLock = new Object(); @@ -176,7 +178,7 @@ public final class NalUnitUtil { while (offset + 1 < length) { int value = data.get(offset) & 0xFF; if (consecutiveZeros == 3) { - if (value == 1 && (data.get(offset + 1) & 0x1F) == NAL_UNIT_TYPE_SPS) { + if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) { // Copy from this NAL unit onwards to the start of the buffer. ByteBuffer offsetData = data.duplicate(); offsetData.position(offset - 3); @@ -197,6 +199,21 @@ public final class NalUnitUtil { data.clear(); } + /** + * Returns whether the NAL unit with the specified header contains supplemental enhancement + * information. + * + * @param mimeType The sample MIME type. + * @param nalUnitHeaderFirstByte The first byte of nal_unit(). + * @return Whether the NAL unit with the specified header is an SEI NAL unit. + */ + public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) { + return (MimeTypes.VIDEO_H264.equals(mimeType) + && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI) + || (MimeTypes.VIDEO_H265.equals(mimeType) + && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI); + } + /** * Returns the type of the NAL unit in {@code data} that starts at {@code offset}. * @@ -297,7 +314,8 @@ public final class NalUnitUtil { int frameCropRightOffset = data.readUnsignedExpGolombCodedInt(); int frameCropTopOffset = data.readUnsignedExpGolombCodedInt(); int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; + int cropUnitX; + int cropUnitY; if (chromaFormatIdc == 0) { cropUnitX = 1; cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); diff --git a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index 19c500202b..08e2bd0669 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -34,21 +34,39 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - private final long firstSampleTimestampUs; - + private long firstSampleTimestampUs; private long timestampOffsetUs; // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. private volatile long lastSampleTimestamp; /** - * @param firstSampleTimestampUs The desired result of the first call to - * {@link #adjustSampleTimestamp(long)}, or {@link #DO_NOT_OFFSET} if presentation timestamps - * should not be offset. + * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. */ public TimestampAdjuster(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; lastSampleTimestamp = C.TIME_UNSET; + setFirstSampleTimestampUs(firstSampleTimestampUs); + } + + /** + * Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be + * called before any timestamps have been adjusted. + * + * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or + * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. + */ + public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { + Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); + this.firstSampleTimestampUs = firstSampleTimestampUs; + } + + /** + * Returns the first adjusted sample timestamp in microseconds. + * + * @return The first adjusted sample timestamp in microseconds. + */ + public long getFirstSampleTimestampUs() { + return firstSampleTimestampUs; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index e854c05165..d9282700d7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; @@ -94,7 +95,7 @@ public final class Util { private static final String TAG = "Util"; 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+))?" + + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" @@ -767,6 +768,40 @@ public final class Util { } } + /** + * Returns the frame size for audio with {@code channelCount} channels in the specified encoding. + * + * @param pcmEncoding The encoding of the audio data. + * @param channelCount The channel count. + * @return The size of one audio frame in bytes. + */ + public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) { + switch (pcmEncoding) { + case C.ENCODING_PCM_8BIT: + return channelCount; + case C.ENCODING_PCM_16BIT: + return channelCount * 2; + case C.ENCODING_PCM_24BIT: + return channelCount * 3; + case C.ENCODING_PCM_32BIT: + return channelCount * 4; + default: + throw new IllegalArgumentException(); + } + } + + /** + * Makes a best guess to infer the type from a {@link Uri}. + * + * @param uri The {@link Uri}. + * @return The content type. + */ + @C.ContentType + public static int inferContentType(Uri uri) { + String path = uri.getPath(); + return path == null ? C.TYPE_OTHER : inferContentType(path); + } + /** * Makes a best guess to infer the type from a file name. * @@ -775,14 +810,14 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - if (fileName == null) { - return C.TYPE_OTHER; - } else if (fileName.endsWith(".mpd")) { + fileName = fileName.toLowerCase(); + if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; - } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml")) { - return C.TYPE_SS; } else if (fileName.endsWith(".m3u8")) { return C.TYPE_HLS; + } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml") + || fileName.endsWith(".ism/manifest") || fileName.endsWith(".isml/manifest")) { + return C.TYPE_SS; } else { return C.TYPE_OTHER; } @@ -924,6 +959,24 @@ public final class Util { throw (T) t; } + /** Recursively deletes a directory and its content. */ + public static void recursiveDelete(File fileOrDirectory) { + if (fileOrDirectory.isDirectory()) { + for (File child : fileOrDirectory.listFiles()) { + recursiveDelete(child); + } + } + fileOrDirectory.delete(); + } + + /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */ + public static File createTempDirectory(Context context, String prefix) throws IOException { + File tempFile = File.createTempFile(prefix, null, context.getCacheDir()); + tempFile.delete(); // Delete the temp file. + tempFile.mkdir(); // Create a directory with the same name. + return tempFile; + } + /** * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" * order. diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 280f004211..059628e0c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -298,13 +298,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void setSurface(Surface surface) throws ExoPlaybackException { - // We only need to release and reinitialize the codec if the surface has changed. + // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { - releaseCodec(); - maybeInitCodec(); + MediaCodec codec = getCodec(); + if (Util.SDK_INT >= 23 && codec != null && surface != null) { + setOutputSurfaceV23(codec, surface); + } else { + releaseCodec(); + maybeInitCodec(); + } } } // Clear state so that we always call the event listener with the video size and when a frame @@ -589,6 +594,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return frameworkMediaFormat; } + @TargetApi(23) + private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { + codec.setOutputSurface(surface); + } + @TargetApi(21) private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) { mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true); diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index c53793b534..cb82d0a466 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 5752058c4e..fc0701da8d 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -15,63 +15,15 @@ */ package com.google.android.exoplayer2.playbacktests.gts; -import android.annotation.TargetApi; -import android.media.MediaDrm; -import android.media.MediaDrm.MediaDrmStateException; -import android.media.UnsupportedSchemeException; -import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; -import android.util.Log; -import android.util.Pair; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.MediaDrmCallback; -import com.google.android.exoplayer2.drm.OfflineLicenseHelper; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import junit.framework.AssertionFailedError; /** * Tests DASH playbacks using {@link ExoPlayer}. @@ -79,147 +31,6 @@ import junit.framework.AssertionFailedError; public final class DashTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "DashTest"; - private static final String VIDEO_TAG = TAG + ":Video"; - private static final String AUDIO_TAG = TAG + ":Audio"; - private static final String REPORT_NAME = "GtsExoPlayerTestCases"; - private static final String REPORT_OBJECT_NAME = "playbacktest"; - private static final int VIDEO_RENDERER_INDEX = 0; - private static final int AUDIO_RENDERER_INDEX = 1; - - private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; - private static final int MIN_LOADABLE_RETRY_COUNT = 10; - private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - - private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-3/screens/dash-vod-single-segment/"; - // Clear content manifests. - private static final String H264_MANIFEST = "manifest-h264.mpd"; - private static final String H265_MANIFEST = "manifest-h265.mpd"; - private static final String VP9_MANIFEST = "manifest-vp9.mpd"; - private static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; - private static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; - private static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; - // Widevine encrypted content manifests. - private static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; - private static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; - private static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; - private static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; - private static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; - private static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; - private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; - private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; - - private static final String AAC_AUDIO_REPRESENTATION_ID = "141"; - private static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; - private static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; - private static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; - private static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; - // The highest quality H264 format mandated by the Android CDD. - private static final String H264_CDD_FIXED = Util.SDK_INT < 23 - ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-23"; - private static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-24"; - private static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-29"; - - private static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; - private static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; - // The highest quality H265 format mandated by the Android CDD. - private static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] H265_CDD_ADAPTIVE = - new String[] { - H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; - private static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; - private static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] VP9_CDD_ADAPTIVE = - new String[] { - VP9_180P_VIDEO_REPRESENTATION_ID, - VP9_360P_VIDEO_REPRESENTATION_ID}; - - // Widevine encrypted content representation ids. - private static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; - private static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; - // The highest quality H264 format mandated by the Android CDD. - private static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 - ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID - : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; - - private static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality H265 format mandated by the Android CDD. - private static final String WIDEVINE_H265_CDD_FIXED = - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] WIDEVINE_H265_CDD_ADAPTIVE = - new String[] { - WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = - new String[] { - WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, - WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_LICENSE_URL = - "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; - private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; - private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; - private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); - private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; - private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; - private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; - - // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD - // if the device advertises support for them. - private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) .delay(10000).seek(15000) @@ -229,40 +40,54 @@ public final class DashTest extends ActivityInstrumentationTestCase2 0) { - synchronized (this) { - wait(licenseDuration * 1000 + 2000); - } - long previousDuration = licenseDuration; - licenseDuration = helper.getLicenseDurationRemainingSec().first; - assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); - } - - // DefaultDrmSessionManager should renew the license and stream play fine - testDashPlayback(getActivity(), streamName, null, true, parameters, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); - } finally { - helper.releaseResources(); - } - } - - public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { - if (Util.SDK_INT < 22) { - // Pass. - return; - } - String streamName = "test_widevine_h264_fixed_offline"; - DashHostedTestEncParameters parameters = newDashHostedTestEncParameters( - WIDEVINE_H264_MANIFEST_PREFIX, true, MimeTypes.VIDEO_H264); - TestOfflineLicenseHelper helper = new TestOfflineLicenseHelper(parameters); - try { - byte[] keySetId = helper.downloadLicense(); - // During playback pause until the license expires then continue playback - Pair licenseDurationRemainingSec = helper.getLicenseDurationRemainingSec(); - long licenseDuration = licenseDurationRemainingSec.first; - assertTrue("License duration should be less than 30 sec. " - + "Server settings might have changed.", licenseDuration < 30); - ActionSchedule schedule = new ActionSchedule.Builder(TAG) - .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); - // DefaultDrmSessionManager should renew the license and stream play fine - testDashPlayback(getActivity(), streamName, schedule, true, parameters, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); - } finally { - helper.releaseResources(); - } + testRunner + .setStreamName("test_widevine_29fps_h264_fixed") + .setManifestUrl(DashTestData.WIDEVINE_H264_29_MANIFEST) + .setWidevineMimeType(MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID) + .run(); } // Internal. - private void testDashPlayback(HostActivity activity, String streamName, String manifestFileName, - String audioFormat, boolean isWidevineEncrypted, String videoMimeType, - boolean canIncludeAdditionalVideoFormats, String... videoFormats) { - testDashPlayback(activity, streamName, null, true, manifestFileName, audioFormat, - isWidevineEncrypted, videoMimeType, canIncludeAdditionalVideoFormats, videoFormats); - } - - private void testDashPlayback(HostActivity activity, String streamName, - ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, String manifestFileName, - String audioFormat, boolean isWidevineEncrypted, String videoMimeType, - boolean canIncludeAdditionalVideoFormats, String... videoFormats) { - testDashPlayback(activity, streamName, actionSchedule, fullPlaybackNoSeeking, - newDashHostedTestEncParameters(manifestFileName, isWidevineEncrypted, videoMimeType), - audioFormat, canIncludeAdditionalVideoFormats, null, videoFormats); - } - - private void testDashPlayback(HostActivity activity, String streamName, - ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, - DashHostedTestEncParameters parameters, String audioFormat, - boolean canIncludeAdditionalVideoFormats, byte[] offlineLicenseKeySetId, - String... videoFormats) { - MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, - REPORT_NAME, REPORT_OBJECT_NAME); - DashHostedTest test = new DashHostedTest(streamName, metricsLogger, fullPlaybackNoSeeking, - audioFormat, canIncludeAdditionalVideoFormats, false, actionSchedule, parameters, - offlineLicenseKeySetId, videoFormats); - activity.runTest(test, TEST_TIMEOUT_MS); - // Retry test exactly once if adaptive test fails due to excessive dropped buffers when playing - // non-CDD required formats (b/28220076). - if (test.needsCddLimitedRetry) { - metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, REPORT_NAME, - REPORT_OBJECT_NAME); - test = new DashHostedTest(streamName, metricsLogger, fullPlaybackNoSeeking, audioFormat, - false, true, actionSchedule, parameters, offlineLicenseKeySetId, videoFormats); - activity.runTest(test, TEST_TIMEOUT_MS); - } - } - - private static DashHostedTestEncParameters newDashHostedTestEncParameters(String manifestFileName, - boolean isWidevineEncrypted, String videoMimeType) { - String manifestPath = MANIFEST_URL_PREFIX + manifestFileName; - return new DashHostedTestEncParameters(manifestPath, isWidevineEncrypted, videoMimeType); - } - private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException { MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, false); assertNotNull(decoderInfo); @@ -778,332 +600,4 @@ public final class DashTest extends ActivityInstrumentationTestCase2 offlineLicenseHelper; - private final DefaultHttpDataSourceFactory httpDataSourceFactory; - private byte[] offlineLicenseKeySetId; - - public TestOfflineLicenseHelper(DashHostedTestEncParameters parameters) - throws UnsupportedDrmException { - this.parameters = parameters; - httpDataSourceFactory = new DefaultHttpDataSourceFactory("ExoPlayerPlaybackTests"); - offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance( - parameters.widevineLicenseUrl, httpDataSourceFactory); - } - - public byte[] downloadLicense() throws InterruptedException, DrmSessionException, IOException { - assertNull(offlineLicenseKeySetId); - offlineLicenseKeySetId = offlineLicenseHelper - .download(httpDataSourceFactory.createDataSource(), parameters.manifestUrl); - assertNotNull(offlineLicenseKeySetId); - assertTrue(offlineLicenseKeySetId.length > 0); - return offlineLicenseKeySetId; - } - - public void renewLicense() throws DrmSessionException { - assertNotNull(offlineLicenseKeySetId); - offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); - assertNotNull(offlineLicenseKeySetId); - } - - public void releaseLicense() throws DrmSessionException { - assertNotNull(offlineLicenseKeySetId); - offlineLicenseHelper.release(offlineLicenseKeySetId); - offlineLicenseKeySetId = null; - } - - public Pair getLicenseDurationRemainingSec() throws DrmSessionException { - return offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); - } - - public void releaseResources() throws DrmSessionException { - if (offlineLicenseKeySetId != null) { - releaseLicense(); - } - if (offlineLicenseHelper != null) { - offlineLicenseHelper.releaseResources(); - } - } - - } - - @TargetApi(16) - private static class DashHostedTest extends ExoHostedTest { - - private final String streamName; - private final MetricsLogger metricsLogger; - private final boolean fullPlaybackNoSeeking; - private final boolean isCddLimitedRetry; - private final DashTestTrackSelector trackSelector; - private final DashHostedTestEncParameters parameters; - private final byte[] offlineLicenseKeySetId; - - private boolean needsCddLimitedRetry; - - /** - * @param streamName The name of the test stream for metric logging. - * @param metricsLogger Logger to log metrics from the test. - * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. - * @param audioFormat The audio format. - * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those - * listed in the videoFormats argument, if the device is capable of playing them. - * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. - * @param actionSchedule The action schedule for the test. - * @param parameters Encryption parameters. - * @param offlineLicenseKeySetId The key set id of the license to be used. - * @param videoFormats The video formats. - */ - public DashHostedTest(String streamName, MetricsLogger metricsLogger, - boolean fullPlaybackNoSeeking, String audioFormat, - boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, - ActionSchedule actionSchedule, DashHostedTestEncParameters parameters, - byte[] offlineLicenseKeySetId, String... videoFormats) { - super(TAG, fullPlaybackNoSeeking); - Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); - this.streamName = streamName; - this.metricsLogger = metricsLogger; - this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; - this.isCddLimitedRetry = isCddLimitedRetry; - this.parameters = parameters; - this.offlineLicenseKeySetId = offlineLicenseKeySetId; - trackSelector = new DashTestTrackSelector(audioFormat, videoFormats, - canIncludeAdditionalVideoFormats); - if (actionSchedule != null) { - setSchedule(actionSchedule); - } - } - - @Override - protected MappingTrackSelector buildTrackSelector(HostActivity host, - BandwidthMeter bandwidthMeter) { - return trackSelector; - } - - @Override - protected final DefaultDrmSessionManager buildDrmSessionManager( - final String userAgent) { - DefaultDrmSessionManager drmSessionManager = null; - if (parameters.isWidevineEncrypted) { - try { - MediaDrmCallback drmCallback = new HttpMediaDrmCallback(parameters.widevineLicenseUrl, - new DefaultHttpDataSourceFactory(userAgent)); - drmSessionManager = DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, - null, null); - if (!parameters.useL1Widevine) { - drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); - } - if (offlineLicenseKeySetId != null) { - drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, - offlineLicenseKeySetId); - } - } catch (UnsupportedDrmException e) { - throw new IllegalStateException(e); - } - } - return drmSessionManager; - } - - @Override - protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { - SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, - new DefaultLoadControl(), drmSessionManager); - player.setVideoSurface(surface); - return player; - } - - @Override - protected MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); - DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, - mediaTransferListener); - Uri manifestUri = Uri.parse(parameters.manifestUrl); - DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( - mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); - } - - @Override - protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { - metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); - metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, - videoCounters.droppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, - videoCounters.maxConsecutiveDroppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, - videoCounters.skippedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, - videoCounters.renderedOutputBufferCount); - metricsLogger.close(); - } - - @Override - protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { - if (fullPlaybackNoSeeking) { - // We shouldn't have skipped any output buffers. - DecoderCountersUtil.assertSkippedOutputBufferCount(AUDIO_TAG, audioCounters, 0); - DecoderCountersUtil.assertSkippedOutputBufferCount(VIDEO_TAG, videoCounters, 0); - // We allow one fewer output buffer due to the way that MediaCodecRenderer and the - // underlying decoders handle the end of stream. This should be tightened up in the future. - DecoderCountersUtil.assertTotalOutputBufferCount(AUDIO_TAG, audioCounters, - audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); - DecoderCountersUtil.assertTotalOutputBufferCount(VIDEO_TAG, videoCounters, - videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); - } - try { - int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); - // Assert that performance is acceptable. - // Assert that total dropped frames were within limit. - DecoderCountersUtil.assertDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - droppedFrameLimit); - // Assert that consecutive dropped frames were within limit. - DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); - } catch (AssertionFailedError e) { - if (trackSelector.includedAdditionalVideoFormats) { - // Retry limiting to CDD mandated formats (b/28220076). - Log.e(TAG, "Too many dropped or consecutive dropped frames.", e); - needsCddLimitedRetry = true; - } else { - throw e; - } - } - } - - } - - private static final class DashTestTrackSelector extends MappingTrackSelector { - - private final String audioFormatId; - private final String[] videoFormatIds; - private final boolean canIncludeAdditionalVideoFormats; - - public boolean includedAdditionalVideoFormats; - - private DashTestTrackSelector(String audioFormatId, String[] videoFormatIds, - boolean canIncludeAdditionalVideoFormats) { - this.audioFormatId = audioFormatId; - this.videoFormatIds = videoFormatIds; - this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; - } - - @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) - throws ExoPlaybackException { - Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_VIDEO); - Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_AUDIO); - Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); - Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); - TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; - selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( - rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, - canIncludeAdditionalVideoFormats), - 0 /* seed */); - selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( - rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), - getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); - includedAdditionalVideoFormats = - selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; - return selections; - } - - private static int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, - String[] formatIds, boolean canIncludeAdditionalFormats) { - List trackIndices = new ArrayList<>(); - - // Always select explicitly listed representations. - for (String formatId : formatIds) { - int trackIndex = getTrackIndex(trackGroup, formatId); - Log.d(TAG, "Adding base video format: " - + Format.toLogString(trackGroup.getFormat(trackIndex))); - trackIndices.add(trackIndex); - } - - // Select additional video representations, if supported by the device. - if (canIncludeAdditionalFormats) { - for (int i = 0; i < trackGroup.length; i++) { - if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { - Log.d(TAG, "Adding extra video format: " - + Format.toLogString(trackGroup.getFormat(i))); - trackIndices.add(i); - } - } - } - - int[] trackIndicesArray = Util.toArray(trackIndices); - Arrays.sort(trackIndicesArray); - return trackIndicesArray; - } - - private static int getTrackIndex(TrackGroup trackGroup, String formatId) { - for (int i = 0; i < trackGroup.length; i++) { - if (trackGroup.getFormat(i).id.equals(formatId)) { - return i; - } - } - throw new IllegalStateException("Format " + formatId + " not found."); - } - - private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED; - } - - } - } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java new file mode 100644 index 0000000000..91e0c163b1 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2017 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.exoplayer2.playbacktests.gts; + +import com.google.android.exoplayer2.util.Util; + +/** + * Test data for {@link DashTest} and {@link DashWidevineOfflineTest). + */ +public final class DashTestData { + + private static final String BASE_URL = "https://storage.googleapis.com/exoplayer-test-media-1/" + + "gen-4/screens/dash-vod-single-segment/"; + // Clear content manifests. + public static final String H264_MANIFEST = BASE_URL + "manifest-h264.mpd"; + public static final String H265_MANIFEST = BASE_URL + "manifest-h265.mpd"; + public static final String VP9_MANIFEST = BASE_URL + "manifest-vp9.mpd"; + public static final String H264_23_MANIFEST = BASE_URL + "manifest-h264-23.mpd"; + public static final String H264_24_MANIFEST = BASE_URL + "manifest-h264-24.mpd"; + public static final String H264_29_MANIFEST = BASE_URL + "manifest-h264-29.mpd"; + // Widevine encrypted content manifests. + public static final String WIDEVINE_H264_MANIFEST = BASE_URL + "manifest-h264-enc.mpd"; + public static final String WIDEVINE_H265_MANIFEST = BASE_URL + "manifest-h265-enc.mpd"; + public static final String WIDEVINE_VP9_MANIFEST = BASE_URL + "manifest-vp9-enc.mpd"; + public static final String WIDEVINE_H264_23_MANIFEST = BASE_URL + "manifest-h264-23-enc.mpd"; + public static final String WIDEVINE_H264_24_MANIFEST = BASE_URL + "manifest-h264-24-enc.mpd"; + public static final String WIDEVINE_H264_29_MANIFEST = BASE_URL + "manifest-h264-29-enc.mpd"; + + public static final String AAC_AUDIO_REPRESENTATION_ID = "141"; + public static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; + public static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; + public static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; + public static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; + // The highest quality H264 format mandated by the Android CDD. + public static final String H264_CDD_FIXED = Util.SDK_INT < 23 + ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-23"; + public static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-24"; + public static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-29"; + + public static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; + public static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; + // The highest quality H265 format mandated by the Android CDD. + public static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] H265_CDD_ADAPTIVE = + new String[] { + H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String VP9_VORBIS_AUDIO_REPRESENTATION_ID = "4"; + public static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; + public static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] VP9_CDD_ADAPTIVE = + new String[] { + VP9_180P_VIDEO_REPRESENTATION_ID, + VP9_360P_VIDEO_REPRESENTATION_ID}; + + // Widevine encrypted content representation ids. + public static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "4"; + public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "5"; + // The highest quality H264 format mandated by the Android CDD. + public static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 + ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID + : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "3"; + + public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "3"; + // The highest quality H265 format mandated by the Android CDD. + public static final String WIDEVINE_H265_CDD_FIXED = + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] WIDEVINE_H265_CDD_ADAPTIVE = + new String[] { + WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_VP9_AAC_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "3"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String WIDEVINE_VP9_CDD_FIXED = WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = + new String[] { + WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, + WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; + + private static final String WIDEVINE_LICENSE_URL = + "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; + private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; + private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; + + public static String getWidevineLicenseUrl(boolean useL1Widevine) { + return WIDEVINE_LICENSE_URL + + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); + } + + private DashTestData() { + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java new file mode 100644 index 0000000000..6374fd97a9 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2017 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.exoplayer2.playbacktests.gts; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; + +import android.annotation.TargetApi; +import android.app.Instrumentation; +import android.media.MediaDrm; +import android.media.UnsupportedSchemeException; +import android.net.Uri; +import android.util.Log; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; +import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; +import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; +import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import junit.framework.AssertionFailedError; + +/** {@link DashHostedTest} builder. */ +public final class DashTestRunner { + + static final int VIDEO_RENDERER_INDEX = 0; + static final int AUDIO_RENDERER_INDEX = 1; + + private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; + + private static final String REPORT_NAME = "GtsExoPlayerTestCases"; + private static final String REPORT_OBJECT_NAME = "playbacktest"; + + // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD + // if the device advertises support for them. + private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; + + private static final String AUDIO_TAG_SUFFIX = ":Audio"; + private static final String VIDEO_TAG_SUFFIX = ":Video"; + + private static final int MIN_LOADABLE_RETRY_COUNT = 10; + private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; + private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; + + private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; + private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; + private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; + + private final String tag; + private final HostActivity activity; + private final Instrumentation instrumentation; + + private String streamName; + private boolean fullPlaybackNoSeeking; + private String audioFormat; + private boolean canIncludeAdditionalVideoFormats; + private ActionSchedule actionSchedule; + private byte[] offlineLicenseKeySetId; + private String[] videoFormats; + private String manifestUrl; + private boolean useL1Widevine; + private String widevineLicenseUrl; + private DataSource.Factory dataSourceFactory; + + @TargetApi(18) + @SuppressWarnings("ResourceType") + public static boolean isL1WidevineAvailable(String mimeType) { + try { + // Force L3 if secure decoder is not available. + if (MediaCodecUtil.getDecoderInfo(mimeType, true) == null) { + return false; + } + MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); + String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); + mediaDrm.release(); + return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); + } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + public DashTestRunner(String tag, HostActivity activity, Instrumentation instrumentation) { + this.tag = tag; + this.activity = activity; + this.instrumentation = instrumentation; + } + + public DashTestRunner setStreamName(String streamName) { + this.streamName = streamName; + return this; + } + + public DashTestRunner setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) { + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + return this; + } + + public DashTestRunner setCanIncludeAdditionalVideoFormats( + boolean canIncludeAdditionalVideoFormats) { + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats + && ALLOW_ADDITIONAL_VIDEO_FORMATS; + return this; + } + + public DashTestRunner setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public DashTestRunner setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) { + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + return this; + } + + public DashTestRunner setAudioVideoFormats(String audioFormat, String... videoFormats) { + this.audioFormat = audioFormat; + this.videoFormats = videoFormats; + return this; + } + + public DashTestRunner setManifestUrl(String manifestUrl) { + this.manifestUrl = manifestUrl; + return this; + } + + public DashTestRunner setWidevineMimeType(String mimeType) { + this.useL1Widevine = isL1WidevineAvailable(mimeType); + this.widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(useL1Widevine); + return this; + } + + public DashTestRunner setDataSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + return this; + } + + public void run() { + DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false, + instrumentation); + activity.runTest(test, TEST_TIMEOUT_MS); + // Retry test exactly once if adaptive test fails due to excessive dropped buffers when + // playing non-CDD required formats (b/28220076). + if (test.needsCddLimitedRetry) { + activity.runTest(createDashHostedTest(false, true, instrumentation), TEST_TIMEOUT_MS); + } + } + + private DashHostedTest createDashHostedTest(boolean canIncludeAdditionalVideoFormats, + boolean isCddLimitedRetry, Instrumentation instrumentation) { + MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(instrumentation, tag, + REPORT_NAME, REPORT_OBJECT_NAME); + return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking, + audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule, + offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, dataSourceFactory, + videoFormats); + } + + /** + * A {@link HostedTest} for DASH playback tests. + */ + @TargetApi(16) + private static final class DashHostedTest extends ExoHostedTest { + + private final String streamName; + private final String manifestUrl; + private final MetricsLogger metricsLogger; + private final boolean fullPlaybackNoSeeking; + private final boolean isCddLimitedRetry; + private final DashTestTrackSelector trackSelector; + private final byte[] offlineLicenseKeySetId; + private final String widevineLicenseUrl; + private final boolean useL1Widevine; + private final DataSource.Factory dataSourceFactory; + + private boolean needsCddLimitedRetry; + + /** + * @param tag A tag to use for logging. + * @param streamName The name of the test stream for metric logging. + * @param manifestUrl The manifest url. + * @param metricsLogger Logger to log metrics from the test. + * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. + * @param audioFormat The audio format. + * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those + * listed in the videoFormats argument, if the device is capable of playing them. + * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. + * @param actionSchedule The action schedule for the test. + * @param offlineLicenseKeySetId The key set id of the license to be used. + * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url + * otherwise null. + * @param useL1Widevine Whether to use L1 Widevine. + * @param dataSourceFactory If not null, used to load manifest and media. + * @param videoFormats The video formats. + */ + private DashHostedTest(String tag, String streamName, String manifestUrl, + MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, + boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, + ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl, + boolean useL1Widevine, DataSource.Factory dataSourceFactory, String... videoFormats) { + super(tag, fullPlaybackNoSeeking); + Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); + this.streamName = streamName; + this.manifestUrl = manifestUrl; + this.metricsLogger = metricsLogger; + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.isCddLimitedRetry = isCddLimitedRetry; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.widevineLicenseUrl = widevineLicenseUrl; + this.useL1Widevine = useL1Widevine; + this.dataSourceFactory = dataSourceFactory; + trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats, + canIncludeAdditionalVideoFormats); + if (actionSchedule != null) { + setSchedule(actionSchedule); + } + } + + @Override + protected MappingTrackSelector buildTrackSelector(HostActivity host, + BandwidthMeter bandwidthMeter) { + return trackSelector; + } + + @Override + protected DefaultDrmSessionManager buildDrmSessionManager( + final String userAgent) { + if (widevineLicenseUrl == null) { + return null; + } + try { + MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, + new DefaultHttpDataSourceFactory(userAgent)); + DefaultDrmSessionManager drmSessionManager = + DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); + if (!useL1Widevine) { + drmSessionManager.setPropertyString( + SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + } + if (offlineLicenseKeySetId != null) { + drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, + offlineLicenseKeySetId); + } + return drmSessionManager; + } catch (UnsupportedDrmException e) { + throw new IllegalStateException(e); + } + } + + @Override + protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, + MappingTrackSelector trackSelector, + DrmSessionManager drmSessionManager) { + SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, + new DefaultLoadControl(), drmSessionManager); + player.setVideoSurface(surface); + return player; + } + + @Override + protected MediaSource buildSource(HostActivity host, String userAgent, + TransferListener mediaTransferListener) { + DataSource.Factory manifestDataSourceFactory = dataSourceFactory != null + ? dataSourceFactory : new DefaultDataSourceFactory(host, userAgent); + DataSource.Factory mediaDataSourceFactory = dataSourceFactory != null + ? dataSourceFactory + : new DefaultDataSourceFactory(host, userAgent, mediaTransferListener); + Uri manifestUri = Uri.parse(manifestUrl); + DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( + mediaDataSourceFactory); + return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + } + + @Override + protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { + metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); + metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, + videoCounters.droppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, + videoCounters.maxConsecutiveDroppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, + videoCounters.skippedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, + videoCounters.renderedOutputBufferCount); + metricsLogger.close(); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + if (fullPlaybackNoSeeking) { + // We shouldn't have skipped any output buffers. + DecoderCountersUtil + .assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0); + DecoderCountersUtil + .assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); + // We allow one fewer output buffer due to the way that MediaCodecRenderer and the + // underlying decoders handle the end of stream. This should be tightened up in the future. + DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, + audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); + DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, + videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); + } + try { + int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION + * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); + // Assert that performance is acceptable. + // Assert that total dropped frames were within limit. + DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, + droppedFrameLimit); + // Assert that consecutive dropped frames were within limit. + DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, + videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); + } catch (AssertionFailedError e) { + if (trackSelector.includedAdditionalVideoFormats) { + // Retry limiting to CDD mandated formats (b/28220076). + Log.e(tag, "Too many dropped or consecutive dropped frames.", e); + needsCddLimitedRetry = true; + } else { + throw e; + } + } + } + + } + + private static final class DashTestTrackSelector extends MappingTrackSelector { + + private final String tag; + private final String audioFormatId; + private final String[] videoFormatIds; + private final boolean canIncludeAdditionalVideoFormats; + + public boolean includedAdditionalVideoFormats; + + private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, + boolean canIncludeAdditionalVideoFormats) { + this.tag = tag; + this.audioFormatId = audioFormatId; + this.videoFormatIds = videoFormatIds; + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_VIDEO); + Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_AUDIO); + Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); + Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); + TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; + selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( + rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, + canIncludeAdditionalVideoFormats), + 0 /* seed */); + selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( + rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), + getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); + includedAdditionalVideoFormats = + selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; + return selections; + } + + private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, + String[] formatIds, boolean canIncludeAdditionalFormats) { + List trackIndices = new ArrayList<>(); + + // Always select explicitly listed representations. + for (String formatId : formatIds) { + int trackIndex = getTrackIndex(trackGroup, formatId); + Log.d(tag, "Adding base video format: " + + Format.toLogString(trackGroup.getFormat(trackIndex))); + trackIndices.add(trackIndex); + } + + // Select additional video representations, if supported by the device. + if (canIncludeAdditionalFormats) { + for (int i = 0; i < trackGroup.length; i++) { + if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { + Log.d(tag, "Adding extra video format: " + + Format.toLogString(trackGroup.getFormat(i))); + trackIndices.add(i); + } + } + } + + int[] trackIndicesArray = Util.toArray(trackIndices); + Arrays.sort(trackIndicesArray); + return trackIndicesArray; + } + + private static int getTrackIndex(TrackGroup trackGroup, String formatId) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i).id.equals(formatId)) { + return i; + } + } + throw new IllegalStateException("Format " + formatId + " not found."); + } + + private static boolean isFormatHandled(int formatSupport) { + return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + == RendererCapabilities.FORMAT_HANDLED; + } + + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java new file mode 100644 index 0000000000..99a6f3bef5 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2017 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.exoplayer2.playbacktests.gts; + +import android.media.MediaDrm.MediaDrmStateException; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Pair; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.OfflineLicenseHelper; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Tests Widevine encrypted DASH playbacks using offline keys. + */ +public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "DashWidevineOfflineTest"; + private static final String USER_AGENT = "ExoPlayerPlaybackTests"; + + private DashTestRunner testRunner; + private DefaultHttpDataSourceFactory httpDataSourceFactory; + private OfflineLicenseHelper offlineLicenseHelper; + private byte[] offlineLicenseKeySetId; + + public DashWidevineOfflineTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) + .setStreamName("test_widevine_h264_fixed_offline") + .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST) + .setWidevineMimeType(MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_CDD_FIXED); + + boolean useL1Widevine = DashTestRunner.isL1WidevineAvailable(MimeTypes.VIDEO_H264); + String widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(useL1Widevine); + httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, + httpDataSourceFactory); + } + + @Override + protected void tearDown() throws Exception { + testRunner = null; + if (offlineLicenseKeySetId != null) { + releaseLicense(); + } + if (offlineLicenseHelper != null) { + offlineLicenseHelper.releaseResources(); + } + offlineLicenseHelper = null; + httpDataSourceFactory = null; + super.tearDown(); + } + + // Offline license tests + + public void testWidevineOfflineLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + testRunner.run(); + + // Renew license after playback should still work + offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); + Assert.assertNotNull(offlineLicenseKeySetId); + } + + public void testWidevineOfflineReleasedLicense() throws Throwable { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + releaseLicense(); // keySetId no longer valid. + + try { + testRunner.run(); + fail("Playback should fail because the license has been released."); + } catch (Throwable e) { + // Get the root cause + while (true) { + Throwable cause = e.getCause(); + if (cause == null || cause == e) { + break; + } + e = cause; + } + // It should be a MediaDrmStateException instance + if (!(e instanceof MediaDrmStateException)) { + throw e; + } + } + } + + public void testWidevineOfflineExpiredLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // Wait until the license expires + long licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + while (licenseDuration > 0) { + synchronized (this) { + wait(licenseDuration * 1000 + 2000); + } + long previousDuration = licenseDuration; + licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); + } + + // DefaultDrmSessionManager should renew the license and stream play fine + testRunner.run(); + } + + public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // During playback pause until the license expires then continue playback + Pair licenseDurationRemainingSec = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); + long licenseDuration = licenseDurationRemainingSec.first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + ActionSchedule schedule = new ActionSchedule.Builder(TAG) + .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); + + // DefaultDrmSessionManager should renew the license and stream play fine + testRunner.setActionSchedule(schedule).run(); + } + + private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { + offlineLicenseKeySetId = offlineLicenseHelper.download( + httpDataSourceFactory.createDataSource(), DashTestData.WIDEVINE_H264_MANIFEST); + Assert.assertNotNull(offlineLicenseKeySetId); + Assert.assertTrue(offlineLicenseKeySetId.length > 0); + testRunner.setOfflineLicenseKeySetId(offlineLicenseKeySetId); + } + + private void releaseLicense() throws DrmSessionException { + offlineLicenseHelper.release(offlineLicenseKeySetId); + offlineLicenseKeySetId = null; + } + +} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java index ede172ad29..c530ab63c1 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java @@ -99,12 +99,14 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer { @Override protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + super.onQueueInputBuffer(buffer); insertTimestamp(buffer.timeUs); maybeShiftTimestampsList(); } @Override protected void onProcessedOutputBuffer(long presentationTimeUs) { + super.onProcessedOutputBuffer(presentationTimeUs); bufferCount++; long expectedTimestampUs = dequeueTimestamp(); if (expectedTimestampUs != presentationTimeUs) { diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 7bf8985b64..87c55e9248 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -34,7 +34,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -63,7 +63,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2; public static final long EXPECTED_PLAYING_TIME_UNSET = -1; - private final String tag; + protected final String tag; + private final boolean failOnPlayerError; private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; @@ -312,7 +313,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen @SuppressWarnings("unused") protected MappingTrackSelector buildTrackSelector(HostActivity host, BandwidthMeter bandwidthMeter) { - return new DefaultTrackSelector(new AdaptiveVideoTrackSelection.Factory(bandwidthMeter)); + return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } @SuppressWarnings("unused") diff --git a/publish.gradle b/publish.gradle new file mode 100644 index 0000000000..17214959ab --- /dev/null +++ b/publish.gradle @@ -0,0 +1,24 @@ +// Copyright (C) 2017 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. +apply plugin: 'bintray-release' + +publish { + artifactId = releaseArtifact + description = releaseDescription + repoName = releaseRepoName + userOrg = releaseUserOrg + groupId = releaseGroupId + version = releaseVersion + website = releaseWebsite +} diff --git a/settings.gradle b/settings.gradle index 8500dc6af7..b69c134fc4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,20 +15,21 @@ include ':library' include ':testutils' include ':demo' include ':playbacktests' +include ':extension-ffmpeg' +include ':extension-flac' +include ':extension-gvr' +include ':extension-okhttp' include ':extension-opus' include ':extension-vp9' -include ':extension-okhttp' -include ':extension-flac' -include ':extension-ffmpeg' // Uncomment the following line to use the Cronet Extension. // include ':extension-cronet' - +project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') +project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') +project(':extension-gvr').projectDir = new File(settingsDir, 'extensions/gvr') +project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') -project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') -project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') -project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') // Uncomment the following line to use the Cronet Extension. // See extensions/cronet/README.md for details. // project(':extension-cronet').projectDir = new File(settingsDir, 'extensions/cronet') diff --git a/testutils/build.gradle b/testutils/build.gradle index 83ff065f9a..a97c743384 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index 3716c6d37f..ee8927ea21 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -47,13 +47,13 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab } @Override - public FakeTrackOutput track(int trackId) { - FakeTrackOutput output = trackOutputs.get(trackId); + public FakeTrackOutput track(int id, int type) { + FakeTrackOutput output = trackOutputs.get(id); if (output == null) { Assert.assertFalse(tracksEnded); numberOfTracks++; output = new FakeTrackOutput(); - trackOutputs.put(trackId, output); + trackOutputs.put(id, output); } return output; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index fd971892b4..75a4a01923 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; -import android.content.Context; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; @@ -25,7 +24,6 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @@ -375,21 +373,4 @@ public class TestUtil { } } - public static void recursiveDelete(File fileOrDirectory) { - if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { - recursiveDelete(child); - } - } - fileOrDirectory.delete(); - } - - /** Creates an empty folder in the application specific cache directory. */ - public static File createTempFolder(Context context) throws IOException { - File tempFolder = File.createTempFile("ExoPlayerTest", null, context.getCacheDir()); - Assert.assertTrue(tempFolder.delete()); - Assert.assertTrue(tempFolder.mkdir()); - return tempFolder; - } - }