diff --git a/README.md b/README.md index d7bc23f700..b21db8b13a 100644 --- a/README.md +++ b/README.md @@ -9,34 +9,37 @@ and extend, and can be updated through Play Store application updates. ## Documentation ## -* The [developer guide][] provides a wealth of information to help you get - started. -* The [class reference][] documents the ExoPlayer library classes. +* The [developer guide][] provides a wealth of information. +* The [class reference][] documents ExoPlayer classes. * The [release notes][] document the major changes in each release. +* Follow our [developer blog][] to keep up to date with the latest ExoPlayer + developments! [developer guide]: https://google.github.io/ExoPlayer/guide.html [class reference]: https://google.github.io/ExoPlayer/doc/reference -[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md +[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md +[developer blog]: https://medium.com/google-exoplayer ## Using ExoPlayer ## -ExoPlayer modules can be obtained via jCenter. It's also possible to clone the +ExoPlayer modules can be obtained from JCenter. It's also possible to clone the repository and depend on the modules locally. -### Via jCenter ### +### From JCenter ### The easiest way to get started using ExoPlayer 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: +dependency. You need to make sure you have the JCenter and Google repositories +included in the `build.gradle` file in the root of your project: ```gradle repositories { jcenter() + google() } ``` Next add a gradle compile dependency to the `build.gradle` file of your app -module. The following will add a dependency to the full ExoPlayer library: +module. The following will add a dependency to the full library: ```gradle compile 'com.google.android.exoplayer:exoplayer:r2.X.X' @@ -53,8 +56,8 @@ compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' ``` -The available modules are listed below. Adding a dependency to the full -ExoPlayer library is equivalent to adding dependencies on all of the modules +The available library modules are listed below. Adding a dependency to the full +library is equivalent to adding dependencies on all of the library modules individually. * `exoplayer-core`: Core functionality (required). @@ -63,11 +66,16 @@ individually. * `exoplayer-smoothstreaming`: Support for SmoothStreaming content. * `exoplayer-ui`: UI components and resources for use with ExoPlayer. -For more details, see the project on [Bintray][]. For information about the -latest versions, see the [Release notes][]. +In addition to library modules, ExoPlayer has multiple extension modules that +depend on external libraries to provide additional functionality. Some +extensions are available from JCenter, whereas others must be built manaully. +Browse the [extensions directory] and their individual READMEs for details. +More information on the library and extension modules that are available from +JCenter can be found on [Bintray][]. + +[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer -[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md ### Locally ### @@ -99,22 +107,16 @@ depend on them as you would on any other local module, for example: ```gradle compile project(':exoplayer-library-core') compile project(':exoplayer-library-dash') -compile project(':exoplayer-library-ui) +compile project(':exoplayer-library-ui') ``` ## Developing ExoPlayer ## #### Project branches #### - * The project has `dev-vX` and `release-vX` branches, where `X` is the major - version number. - * Most development work happens on the `dev-vX` branch with the highest major - version number. Pull requests should normally be made to this branch. - * Bug fixes may be submitted to older `dev-vX` branches. When doing this, the - same (or an equivalent) fix should also be submitted to all subsequent - `dev-vX` branches. - * A `release-vX` branch holds the most recent stable release for major version - `X`. +* Development work happens on the `dev-v2` branch. Pull requests should + normally be made to this branch. +* The `release-v2` branch holds the most recent release. #### Using Android Studio #### diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ad866395e..b694143542 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,31 @@ # Release notes # +### r2.5.2 ### + +* IMA extension: Fix issue where ad playback could end prematurely for some + content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). +* RTMP extension: Fix SIGABRT on fast RTMP stream restart + ([#3156](https://github.com/google/ExoPlayer/issues/3156)). +* UI: Allow app to manually specify ad markers + ([#3184](https://github.com/google/ExoPlayer/issues/3184)). +* DASH: Expose segment indices to subclasses of DefaultDashChunkSource + ([#3037](https://github.com/google/ExoPlayer/issues/3037)). +* Captions: Added robustness against malformed WebVTT captions + ([#3228](https://github.com/google/ExoPlayer/issues/3228)). +* DRM: Support forcing a specific license URL. +* Fix playback error when seeking in media loaded through content:// URIs + ([#3216](https://github.com/google/ExoPlayer/issues/3216)). +* Fix issue playing MP4s in which the last atom specifies a size of zero + ([#3191](https://github.com/google/ExoPlayer/issues/3191)). +* Workaround playback failures on some Xiaomi devices + ([#3171](https://github.com/google/ExoPlayer/issues/3171)). +* Workaround SIGSEGV issue on some devices when setting and swapping surface for + secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)). +* Workaround for Nexus 7 issue when swapping output surface + ([#3236](https://github.com/google/ExoPlayer/issues/3236)). +* Workaround for SimpleExoPlayerView's surface not being hidden properly + ([#3160](https://github.com/google/ExoPlayer/issues/3160)). + ### r2.5.1 ### * Fix an issue that could cause the reported playback position to stop advancing diff --git a/build.gradle b/build.gradle index dbc8a41eb0..8ec24a6e82 100644 --- a/build.gradle +++ b/build.gradle @@ -14,9 +14,12 @@ buildscript { repositories { jcenter() + maven { + url "https://maven.google.com" + } } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.android.tools.build:gradle:3.0.0-beta4' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: diff --git a/constants.gradle b/constants.gradle index b7cc8b6906..7391228853 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.1' + releaseVersion = 'r2.5.2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/README.md b/demo/README.md index ca37392623..bdb04e5ba8 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,5 +1,5 @@ -# Demo application # +# ExoPlayer main demo # -This folder contains a demo application that uses ExoPlayer to play a number +This is the main ExoPlayer demo application. It uses ExoPlayer to play a number of test streams. It can be used as a starting point or reference project when developing other applications that make use of the ExoPlayer library. diff --git a/demo/build.gradle b/demo/build.gradle index 7eea25478f..e0874e3147 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -39,9 +39,15 @@ android { disable 'MissingTranslation' } + flavorDimensions "extensions" + productFlavors { - noExtensions - withExtensions + noExtensions { + dimension "extensions" + } + withExtensions { + dimension "extensions" + } } } @@ -56,4 +62,5 @@ dependencies { withExtensionsCompile project(path: modulePrefix + 'extension-ima') withExtensionsCompile project(path: modulePrefix + 'extension-opus') withExtensionsCompile project(path: modulePrefix + 'extension-vp9') + withExtensionsCompile project(path: modulePrefix + 'extension-rtmp') } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1f66822dc7..081ca00077 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2502" + android:versionName="2.5.2"> 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 30dfb5140a..2ea4b5b7cf 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 @@ -431,6 +431,8 @@ import java.util.Locale; return "YES"; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: return "NO_UNSUPPORTED_TYPE"; case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: 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 9e53dff857..6416cd5aa2 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 @@ -294,9 +294,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); player.addListener(eventLogger); + player.addMetadataOutput(eventLogger); player.setAudioDebugListener(eventLogger); player.setVideoDebugListener(eventLogger); - player.setMetadataOutput(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 0000000000..bf0effb358 --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,5 @@ +# ExoPlayer extensions # + +ExoPlayer extensions are modules that depend on external libraries to provide +additional functionality. Browse the individual extensions and their READMEs to +learn more. diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 2287c4c19b..66da774978 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -1,10 +1,8 @@ # ExoPlayer Cronet extension # -## Description ## - The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. -[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html +[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F ## Build instructions ## @@ -22,12 +20,9 @@ and enable the extension: 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension -* In your `settings.gradle` file, add the following line before the line that - applies `core_settings.gradle`: - -```gradle -gradle.ext.exoplayerIncludeCronetExtension = true; -``` +* In your `settings.gradle` file, add + `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that + applies `core_settings.gradle`. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android @@ -56,3 +51,10 @@ new DefaultDataSourceFactory( new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */); ``` respectively. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index b4514effbc..57b637d1e2 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -1,18 +1,17 @@ # ExoPlayer FFmpeg extension # -## Description ## - -The FFmpeg extension is a [Renderer][] implementation that uses FFmpeg to decode -audio. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for +decoding and can render audio encoded in a variety of formats. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. The extension is not provided via JCenter (see [#2781][] +for more information). + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -34,7 +33,11 @@ NDK_PATH="" HOST_PLATFORM="linux-x86_64" ``` -* Fetch and build FFmpeg. For example, to fetch and build for armeabi-v7a, +* Fetch and build FFmpeg. The configuration flags determine which formats will + be supported. See the [Supported formats][] page for more details of the + available flags. + +For example, to fetch and build for armeabi-v7a, arm64-v8a and x86 on Linux x86_64: ``` @@ -103,5 +106,42 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return an + `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `FfmpegAudioRenderer` to the player, +then implement your own logic to use the renderer for a given track. + [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#2781]: https://github.com/google/ExoPlayer/issues/2781 +[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 9db2e5727d..113b41a93d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -1,19 +1,16 @@ # ExoPlayer Flac extension # -## Description ## - -The Flac extension is a [Renderer][] implementation that helps you bundle -libFLAC (the Flac decoding library) into your app and use it along with -ExoPlayer to play Flac audio on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which +use libFLAC (the Flac decoding library) to extract and decode FLAC audio. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -46,3 +43,47 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use the extractor and/or +renderer. + +### Using `FlacExtractor` ### + +`FlacExtractor` is used via `ExtractorMediaSource`. If you're using +`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read +`.flac` files. If you're not using `DefaultExtractorsFactory`, return a +`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation. + +### Using `LibflacAudioRenderer` ### + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibflacAudioRenderer` to the +player, then implement your own logic to use the renderer for a given track. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 7e072d070c..250cf58c2f 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -1,7 +1,5 @@ # 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. @@ -11,17 +9,7 @@ of surround sound and ambisonic soundfields. ## Getting 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: +The easiest way to use the extension is to add it as a gradle dependency: ```gradle compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' @@ -36,9 +24,17 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## -* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to - return a GvrAudioProcessor. -* If constructing renderers directly, pass a GvrAudioProcessor to - MediaCodecAudioRenderer's constructor. +* If using `DefaultRenderersFactory`, override + `DefaultRenderersFactory.buildAudioProcessors` to return a + `GvrAudioProcessor`. +* If constructing renderers directly, pass a `GvrAudioProcessor` to + `MediaCodecAudioRenderer`'s constructor. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ima/README.md b/extensions/ima/README.md index f328bb44cb..4f63214f04 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -1,7 +1,5 @@ # ExoPlayer IMA extension # -## Description ## - The IMA extension is a [MediaSource][] implementation wrapping the [Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads alongside content. @@ -55,3 +53,10 @@ playback. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a4ead9e01f..c9285162a8 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -1,3 +1,16 @@ +// 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 from: '../../constants.gradle' apply plugin: 'com.android.library' diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 8c4fb4c51c..11aab906e0 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -430,7 +430,9 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } else if (!playingAd) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { - return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); + long adDuration = player.getDuration(); + return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY + : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); } } diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 3acf8e4c79..3278e8dba5 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -1,11 +1,9 @@ # ExoPlayer MediaSession extension # -## Description ## - -The MediaSession extension mediates between an ExoPlayer instance and a -[MediaSession][]. It automatically retrieves and implements playback actions -and syncs the player state with the state of the media session. The behaviour -can be extended to support other playback and custom actions. +The MediaSession extension mediates between a Player (or ExoPlayer) instance +and a [MediaSession][]. It automatically retrieves and implements playback +actions and syncs the player state with the state of the media session. The +behaviour can be extended to support other playback and custom actions. [MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html @@ -25,3 +23,10 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching + `com.google.android.exoplayer2.ext.mediasession.*` belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 0e839b8083..329d446506 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -47,7 +47,7 @@ import java.util.Map; * Connects a {@link MediaSessionCompat} to a {@link Player}. *

* The connector listens for actions sent by the media session's controller and implements these - * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * actions by calling appropriate player methods. The playback state of the media session is * automatically synced with the player. The connector can also be optionally extended by providing * various collaborators: *

    @@ -73,6 +73,10 @@ public final class MediaSessionConnector { } public static final String EXTRAS_PITCH = "EXO_PITCH"; + private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; + private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS + | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; /** * Interface to which playback preparation actions are delegated. @@ -318,7 +322,6 @@ public final class MediaSessionConnector { private Player player; private CustomActionProvider[] customActionProviders; - private int currentWindowIndex; private Map customActionMap; private ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; @@ -369,8 +372,7 @@ public final class MediaSessionConnector { this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS); mediaController = mediaSession.getController(); mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); @@ -433,6 +435,8 @@ public final class MediaSessionConnector { */ public void setQueueEditor(QueueEditor queueEditor) { this.queueEditor = queueEditor; + mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS + : EDITOR_MEDIA_SESSION_FLAGS); } private void updateMediaSessionPlaybackState() { @@ -583,11 +587,20 @@ public final class MediaSessionConnector { private class ExoPlayerEventListener implements Player.EventListener { + private int currentWindowIndex; + private int currentWindowCount; + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { if (queueNavigator != null) { queueNavigator.onTimelineChanged(player); } + int windowCount = player.getCurrentTimeline().getWindowCount(); + if (currentWindowCount != windowCount) { + // active queue item and queue navigation actions may need to be updated + updateMediaSessionPlaybackState(); + } + currentWindowCount = windowCount; currentWindowIndex = player.getCurrentWindowIndex(); updateMediaSessionMetadata(); } diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index b10c4ba629..f84d0c35f2 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -1,7 +1,5 @@ # ExoPlayer OkHttp extension # -## Description ## - The OkHttp extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. @@ -49,3 +47,10 @@ new DefaultDataSourceFactory( new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */); ``` respectively. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/opus/README.md b/extensions/opus/README.md index e5f5bcb168..d766e8c9c4 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -1,19 +1,16 @@ # ExoPlayer Opus extension # -## Description ## - -The Opus extension is a [Renderer][] implementation that helps you bundle -libopus (the Opus decoding library) into your app and use it along with -ExoPlayer to play Opus audio on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus +decoding library) to decode Opus audio. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -59,3 +56,38 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 * Clean and re-build the project. * If you want to use your own version of libopus, place it in `${OPUS_EXT_PATH}/jni/libopus`. + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibopusAudioRenderer` to the +player, then implement your own logic to use the renderer for a given track. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 042d7078dc..7e6bc0d641 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -1,7 +1,5 @@ # ExoPlayer RTMP extension # -## Description ## - The RTMP extension is a [DataSource][] implementation for playing [RTMP][] streams using [LibRtmp Client for Android][]. @@ -9,7 +7,7 @@ streams using [LibRtmp Client for Android][]. [RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol [LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android -## Using the extension ## +## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: @@ -25,3 +23,26 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +`DefaultDataSource` will automatically use uses the RTMP extension whenever it's +available. Hence if your application is using `DefaultDataSource` or +`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as +adding a dependency to the RTMP extension as described above. No changes to your +application code are required. Alternatively, if you know that your application +doesn't need to handle any other protocols, you can update any `DataSource`s and +`DataSource.Factory` instantiations in your application code to use +`RtmpDataSource` and `RtmpDataSourceFactory` directly. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index c832cb82e9..7687f03e32 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -26,7 +26,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') - compile 'net.butterflytv.utils:rtmp-client:0.2.8' + compile 'net.butterflytv.utils:rtmp-client:3.0.0' } ext { diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 87c5c8d54f..7bce4a2a25 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -1,19 +1,16 @@ # ExoPlayer VP9 extension # -## Description ## - -The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx -(the VP9 decoding library) into your app and use it along with ExoPlayer to play -VP9 video on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx +decoding library) to decode VP9 video. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -76,3 +73,45 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 `${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. + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't + support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER` + to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer` + to the output list in `buildVideoRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibvpxVideoRenderer` to the +player, then implement your own logic to use the renderer for a given track. + +`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not +being used via `SimpleExoPlayer`, in which case color space conversion will be +performed using a GL shader. To enable this mode, send the renderer a message of +type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the +`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a +`Surface`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index d02d524713..f0b93b1dc2 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -197,12 +197,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { 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 + int sampleY = 0; 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); @@ -210,12 +210,14 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { 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. + sampleY += *srcBase++; + *destBase++ = sampleY >> 2; + sampleY = sampleY & 3; // Remainder. } } // UV + int sampleU = 0; + int sampleV = 0; const int32_t uvWidth = (img->d_w + 1) / 2; for (int y = 0; y < uvHeight; y++) { const uint16_t* srcUBase = reinterpret_cast( @@ -228,11 +230,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { 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. + sampleU += *srcUBase++; + *destUBase++ = sampleU >> 2; + sampleU = sampleU & 3; // Remainder. + sampleV += *srcVBase++; + *destVBase++ = sampleV >> 2; + sampleV = sampleV & 3; // Remainder. } } } else { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc42154505..32ec7e3327 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 12 10:31:13 BST 2017 +#Tue Sep 05 13:43:42 BST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/library/README.md b/library/README.md new file mode 100644 index 0000000000..7aa07fc521 --- /dev/null +++ b/library/README.md @@ -0,0 +1,7 @@ +# ExoPlayer library # + +The ExoPlayer library is split into multiple modules. See ExoPlayer's +[top level README][] for more information about the available library modules +and how to use them. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/library/all/README.md b/library/all/README.md new file mode 100644 index 0000000000..8746e3afc6 --- /dev/null +++ b/library/all/README.md @@ -0,0 +1,13 @@ +# ExoPlayer full library # + +An empty module that depends on all of the other library modules. Depending on +the full library is equivalent to depending on all of the other library modules +individually. See ExoPlayer's [top level README][] for more information. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/core/README.md b/library/core/README.md new file mode 100644 index 0000000000..f31ffed131 --- /dev/null +++ b/library/core/README.md @@ -0,0 +1,9 @@ +# ExoPlayer core library module # + +The core of the ExoPlayer library. + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 b/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 deleted file mode 100644 index 3d3c63786e..0000000000 Binary files a/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 and /dev/null differ diff --git a/library/core/src/androidTest/assets/subrip/typical_unexpected_end b/library/core/src/androidTest/assets/subrip/typical_unexpected_end new file mode 100644 index 0000000000..8e2949b8db --- /dev/null +++ b/library/core/src/androidTest/assets/subrip/typical_unexpected_end @@ -0,0 +1,10 @@ +1 +00:00:00,000 --> 00:00:01,234 +This is the first subtitle. + +2 +00:00:02,345 --> 00:00:03,456 +This is the second subtitle. +Second subtitle with second line. + +3 \ No newline at end of file diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 76c13495c1..c9364aa605 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.mp4; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; @@ -38,11 +37,6 @@ public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } - public void testAtomWithZeroSize() throws Exception { - ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", - getInstrumentation(), ParserException.class); - } - private static ExtractorFactory getExtractorFactory() { return getExtractorFactory(0); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 167499fcdc..744634edda 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -31,6 +31,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode"; private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence"; private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; + private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; public void testDecodeEmpty() throws IOException { @@ -107,6 +108,17 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 0); } + public void testDecodeTypicalUnexpectedEnd() throws IOException { + // Parsing should succeed, parsing the first and second cues only. + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_UNEXPECTED_END); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(4, subtitle.getEventTimeCount()); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + } + public void testDecodeNoEndTimecodes() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 834e7e1374..2b70c83ca5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,9 +23,12 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Arrays; /** * Unit tests for {@link ContentDataSource}. @@ -35,6 +38,9 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + private static final int TEST_DATA_OFFSET = 1; + private static final int TEST_DATA_LENGTH = 1023; + public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() @@ -64,6 +70,27 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } + public void testReadFromOffsetToEndOfInput() throws Exception { + ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + Uri contentUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .path(DATA_PATH).build(); + try { + DataSpec dataSpec = new DataSpec(contentUri, TEST_DATA_OFFSET, C.LENGTH_UNSET, null); + long length = dataSource.open(dataSpec); + assertEquals(TEST_DATA_LENGTH, length); + byte[] expectedData = Arrays.copyOfRange( + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), TEST_DATA_OFFSET, + TEST_DATA_OFFSET + TEST_DATA_LENGTH); + byte[] readData = TestUtil.readToEnd(dataSource); + MoreAsserts.assertEquals(expectedData, readData); + assertEquals(C.RESULT_END_OF_INPUT, dataSource.read(new byte[1], 0, 1)); + } finally { + dataSource.close(); + } + } + /** * A {@link ContentProvider} for the test. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index c3a76cd962..b53cce1f74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -319,8 +319,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.positionUs); } } @@ -330,8 +329,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.bufferedPositionUs); } } @@ -358,7 +356,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + return pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); } @Override @@ -448,6 +446,12 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; + if (timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline is empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } if (msg.arg1 != 0) { for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -472,6 +476,12 @@ import java.util.concurrent.CopyOnWriteArraySet; timeline = sourceInfo.timeline; manifest = sourceInfo.manifest; playbackInfo = sourceInfo.playbackInfo; + if (pendingSeekAcks == 0 && timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline is empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } for (Player.EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } @@ -500,4 +510,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { + long positionMs = C.usToMs(positionUs); + if (!playbackInfo.periodId.isAd()) { + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + positionMs += period.getPositionInWindowMs(); + } + return positionMs; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a789dbc1b2..b8274126b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -263,13 +263,18 @@ import java.io.IOException; } int messageNumber = customMessagesSent++; handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + boolean wasInterrupted = false; while (customMessagesProcessed <= messageNumber) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } public synchronized void release() { @@ -277,13 +282,18 @@ import java.io.IOException; return; } handler.sendEmptyMessage(MSG_RELEASE); + boolean wasInterrupted = false; while (!released) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } internalPlaybackThread.quit(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 33f992964a..98eeb99ad8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.1"; + public static final String VERSION = "2.5.2"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005001; + public static final int VERSION_INT = 2005002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index f841a1b8b5..3f1be20cfb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -24,7 +24,7 @@ public interface RendererCapabilities { /** * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, + * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. */ int FORMAT_SUPPORT_MASK = 0b111; @@ -117,8 +117,8 @@ public interface RendererCapabilities { * the bitwise OR of three properties: *
      *
    • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link #FORMAT_UNSUPPORTED_TYPE}.
    • + * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. *
    • The level of support for adapting from the format to another format of the same mime type. * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and * {@link #ADAPTIVE_NOT_SUPPORTED}.
    • diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 3a3768bcc2..1887e0d243 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can @@ -87,6 +88,9 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; + private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet textOutputs; + private final CopyOnWriteArraySet metadataOutputs; private final int videoRendererCount; private final int audioRendererCount; @@ -99,9 +103,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private TextRenderer.Output textOutput; - private MetadataRenderer.Output metadataOutput; - private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; @@ -113,6 +114,9 @@ public class SimpleExoPlayer implements ExoPlayer { protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { componentListener = new ComponentListener(); + videoListeners = new CopyOnWriteArraySet<>(); + textOutputs = new CopyOnWriteArraySet<>(); + metadataOutputs = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -440,63 +444,132 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Sets a listener to receive video events. + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + public void addVideoListener(VideoListener listener) { + videoListeners.add(listener); + } + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + public void removeVideoListener(VideoListener listener) { + videoListeners.remove(listener); + } + + /** + * Sets a listener to receive video events, removing all existing listeners. * * @param listener The listener. + * @deprecated Use {@link #addVideoListener(VideoListener)}. */ + @Deprecated public void setVideoListener(VideoListener listener) { - videoListener = listener; + videoListeners.clear(); + if (listener != null) { + addVideoListener(listener); + } } /** - * Clears the listener receiving video events if it matches the one passed. Else does nothing. + * Equivalent to {@link #removeVideoListener(VideoListener)}. * * @param listener The listener to clear. + * @deprecated Use {@link #removeVideoListener(VideoListener)}. */ + @Deprecated public void clearVideoListener(VideoListener listener) { - if (videoListener == listener) { - videoListener = null; - } + removeVideoListener(listener); } /** - * Sets an output to receive text events. + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + public void addTextOutput(TextRenderer.Output listener) { + textOutputs.add(listener); + } + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + public void removeTextOutput(TextRenderer.Output listener) { + textOutputs.remove(listener); + } + + /** + * Sets an output to receive text events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addTextOutput(TextRenderer.Output)}. */ + @Deprecated public void setTextOutput(TextRenderer.Output output) { - textOutput = output; - } - - /** - * Clears the output receiving text events if it matches the one passed. Else does nothing. - * - * @param output The output to clear. - */ - public void clearTextOutput(TextRenderer.Output output) { - if (textOutput == output) { - textOutput = null; + textOutputs.clear(); + if (output != null) { + addTextOutput(output); } } /** - * Sets a listener to receive metadata events. + * Equivalent to {@link #removeTextOutput(TextRenderer.Output)}. + * + * @param output The output to clear. + * @deprecated Use {@link #removeTextOutput(TextRenderer.Output)}. + */ + @Deprecated + public void clearTextOutput(TextRenderer.Output output) { + removeTextOutput(output); + } + + /** + * Registers an output to receive metadata events. + * + * @param listener The output to register. + */ + public void addMetadataOutput(MetadataRenderer.Output listener) { + metadataOutputs.add(listener); + } + + /** + * Removes a metadata output. + * + * @param listener The output to remove. + */ + public void removeMetadataOutput(MetadataRenderer.Output listener) { + metadataOutputs.remove(listener); + } + + /** + * Sets an output to receive metadata events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addMetadataOutput(MetadataRenderer.Output)}. */ + @Deprecated public void setMetadataOutput(MetadataRenderer.Output output) { - metadataOutput = output; + metadataOutputs.clear(); + if (output != null) { + addMetadataOutput(output); + } } /** - * Clears the output receiving metadata events if it matches the one passed. Else does nothing. + * Equivalent to {@link #removeMetadataOutput(MetadataRenderer.Output)}. * * @param output The output to clear. + * @deprecated Use {@link #removeMetadataOutput(MetadataRenderer.Output)}. */ + @Deprecated public void clearMetadataOutput(MetadataRenderer.Output output) { - if (metadataOutput == output) { - metadataOutput = null; - } + removeMetadataOutput(output); } /** @@ -803,7 +876,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (videoListener != null) { + for (VideoListener videoListener : videoListeners) { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -815,8 +888,10 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onRenderedFirstFrame(Surface surface) { - if (videoListener != null && SimpleExoPlayer.this.surface == surface) { - videoListener.onRenderedFirstFrame(); + if (SimpleExoPlayer.this.surface == surface) { + for (VideoListener videoListener : videoListeners) { + videoListener.onRenderedFirstFrame(); + } } if (videoDebugListener != null) { videoDebugListener.onRenderedFirstFrame(surface); @@ -889,7 +964,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onCues(List cues) { - if (textOutput != null) { + for (TextRenderer.Output textOutput : textOutputs) { textOutput.onCues(cues); } } @@ -898,7 +973,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onMetadata(Metadata metadata) { - if (metadataOutput != null) { + for (MetadataRenderer.Output metadataOutput : metadataOutputs) { metadataOutput.onMetadata(metadata); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 414c0804ad..7d4c1995eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -593,30 +593,6 @@ public abstract class Timeline { } } - /** - * Returns whether the given window is the last window of the timeline depending on the - * {@code repeatMode}. - * - * @param windowIndex A window index. - * @param repeatMode A repeat mode. - * @return Whether the window of the given index is the last window of the timeline. - */ - public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) { - return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; - } - - /** - * Returns whether the given window is the first window of the timeline depending on the - * {@code repeatMode}. - * - * @param windowIndex A window index. - * @param repeatMode A repeat mode. - * @return Whether the window of the given index is the first window of the timeline. - */ - public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) { - return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; - } - /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index ef7877ae1e..5c5ac06da3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -241,7 +241,7 @@ import java.util.Arrays; for (int i = 0; i < period; i++) { short sVal = samples[position + i]; short pVal = samples[position + period + i]; - diff += sVal >= pVal ? sVal - pVal : pVal - sVal; + diff += Math.abs(sVal - pVal); } // Note that the highest number of samples we add into diff will be less than 256, since we // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java index 6916b972b2..81cfc26393 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java @@ -1,20 +1,37 @@ +/* + * 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.drm; /** - * An exception when doing drm decryption using the In-App Drm + * Thrown when a non-platform component fails to decrypt data. */ public class DecryptionException extends Exception { - private final int errorCode; + /** + * A component specific error code. + */ + public final int errorCode; + + /** + * @param errorCode A component specific error code. + * @param message The detail message. + */ public DecryptionException(int errorCode, String message) { super(message); this.errorCode = errorCode; } - /** - * Get error code - */ - public int getErrorCode() { - return errorCode; - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 0c17b102fd..a3ae1d8b71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -28,7 +28,9 @@ import java.util.Map; @TargetApi(16) public interface DrmSession { - /** Wraps the throwable which is the cause of the error state. */ + /** + * Wraps the throwable which is the cause of the error state. + */ class DrmSessionException extends Exception { public DrmSessionException(Throwable cause) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index f08d9b59b5..dfbf3dee07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,7 +24,6 @@ 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; @@ -39,33 +38,33 @@ import java.util.UUID; public final class HttpMediaDrmCallback implements MediaDrmCallback { private final HttpDataSource.Factory dataSourceFactory; - private final String defaultUrl; + private final String defaultLicenseUrl; + private final boolean forceDefaultLicenseUrl; private final Map keyRequestProperties; /** - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. */ - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { - this(defaultUrl, dataSourceFactory, null); + public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { + this(defaultLicenseUrl, false, dataSourceFactory); } /** - * @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 defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is + * set to true. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own 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) { + public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl, + HttpDataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.defaultUrl = defaultUrl; + this.defaultLicenseUrl = defaultLicenseUrl; + this.forceDefaultLicenseUrl = forceDefaultLicenseUrl; this.keyRequestProperties = new HashMap<>(); - if (keyRequestProperties != null) { - this.keyRequestProperties.putAll(keyRequestProperties); - } } /** @@ -112,8 +111,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { String url = request.getDefaultUrl(); - if (TextUtils.isEmpty(url)) { - url = defaultUrl; + if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { + url = defaultLicenseUrl; } Map requestProperties = new HashMap<>(); // Add standard request properties for supported schemes. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 040ca50c76..62e7f5ed29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -44,23 +44,47 @@ public final class OfflineLicenseHelper { * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. * - * @param licenseUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. */ public static OfflineLicenseHelper newWidevineInstance( - String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { - return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); + String defaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); } /** * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. * - * @param callback Performs key and provisioning requests. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + */ + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, + null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @return A new instance which uses Widevine CDM. @@ -70,9 +94,11 @@ public final class OfflineLicenseHelper { * MediaDrmCallback, HashMap, Handler, EventListener) */ public static OfflineLicenseHelper newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, + HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } @@ -116,9 +142,32 @@ public final class OfflineLicenseHelper { optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); } - /** Releases the helper. Should be called when the helper is no longer required. */ - public void release() { - handlerThread.quit(); + /** + * @see DefaultDrmSessionManager#getPropertyByteArray + */ + public synchronized byte[] getPropertyByteArray(String key) { + return drmSessionManager.getPropertyByteArray(key); + } + + /** + * @see DefaultDrmSessionManager#setPropertyByteArray + */ + public synchronized void setPropertyByteArray(String key, byte[] value) { + drmSessionManager.setPropertyByteArray(key, value); + } + + /** + * @see DefaultDrmSessionManager#getPropertyString + */ + public synchronized String getPropertyString(String key) { + return drmSessionManager.getPropertyString(key); + } + + /** + * @see DefaultDrmSessionManager#setPropertyString + */ + public synchronized void setPropertyString(String key, String value) { + drmSessionManager.setPropertyString(key, value); } /** @@ -186,6 +235,13 @@ public final class OfflineLicenseHelper { return licenseDurationRemainingSec; } + /** + * Releases the helper. Should be called when the helper is no longer required. + */ + public void release() { + handlerThread.quit(); + } + private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index cc7e662336..21d861af30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -39,9 +39,14 @@ import java.util.List; public static final int LONG_HEADER_SIZE = 16; /** - * Value for the first 32 bits of atomSize when the atom size is actually a long value. + * Value for the size field in an atom that defines its size in the largesize field. */ - public static final int LONG_SIZE_PREFIX = 1; + public static final int DEFINES_LARGE_SIZE = 1; + + /** + * Value for the size field in an atom that extends to the end of the file. + */ + public static final int EXTENDS_TO_END_SIZE = 0; public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index f7e3e846e9..9a03311ccf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -995,9 +995,10 @@ import java.util.List; int objectTypeIndication = parent.readUnsignedByte(); String mimeType; switch (objectTypeIndication) { - case 0x6B: - mimeType = MimeTypes.AUDIO_MPEG; - return Pair.create(mimeType, null); + case 0x60: + case 0x61: + mimeType = MimeTypes.VIDEO_MPEG2; + break; case 0x20: mimeType = MimeTypes.VIDEO_MP4V; break; @@ -1007,6 +1008,9 @@ import java.util.List; case 0x23: mimeType = MimeTypes.VIDEO_H265; break; + case 0x6B: + mimeType = MimeTypes.AUDIO_MPEG; + return Pair.create(mimeType, null); case 0x40: case 0x66: case 0x67: @@ -1034,8 +1038,8 @@ import java.util.List; parent.skipBytes(12); - // Start of the AudioSpecificConfig. - parent.skipBytes(1); // AudioSpecificConfig tag + // Start of the DecoderSpecificInfo. + parent.skipBytes(1); // DecoderSpecificInfo tag int initializationDataSize = parseExpandableClassSize(parent); byte[] initializationData = new byte[initializationDataSize]; parent.readBytes(initializationData, 0, initializationDataSize); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 6b2077ef76..c3f2a9fb38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -283,12 +283,22 @@ public final class FragmentedMp4Extractor implements Extractor { atomType = atomHeader.readInt(); } - if (atomSize == Atom.LONG_SIZE_PREFIX) { - // Read the extended atom size. + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); atomHeaderBytesRead += headerBytesRemaining; atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } } if (atomSize < atomHeaderBytesRead) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index d0e770abdc..d3fe9e0d05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -205,12 +205,26 @@ public final class Mp4Extractor implements Extractor, SeekMap { atomType = atomHeader.readInt(); } - if (atomSize == Atom.LONG_SIZE_PREFIX) { - // Read the extended atom size. + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); atomHeaderBytesRead += headerBytesRemaining; atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } + } + + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); } if (shouldParseContainerAtom(atomType)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 44d5824945..021c9de654 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -104,11 +104,18 @@ import java.io.IOException; input.peekFully(buffer.data, 0, headerSize); long atomSize = buffer.readUnsignedInt(); int atomType = buffer.readInt(); - if (atomSize == Atom.LONG_SIZE_PREFIX) { + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large atom size. headerSize = Atom.LONG_HEADER_SIZE; input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); buffer.setLimit(Atom.LONG_HEADER_SIZE); atomSize = buffer.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. + long endPosition = input.getLength(); + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + headerSize; + } } if (atomSize < headerSize) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index d3f3dae344..1073e8d9c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -288,9 +288,11 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/1528 + // Work around https://github.com/google/ExoPlayer/issues/1528 and + // https://github.com/google/ExoPlayer/issues/3171 if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) - && "a70".equals(Util.DEVICE)) { + && ("a70".equals(Util.DEVICE) + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 7a43dd7562..514b96ae8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -16,12 +16,15 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; /** - * A source of a single period of media. + * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All + * methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaPeriod extends SequenceableLoader { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 790620a80c..11489cfbb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -23,7 +23,19 @@ import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; /** - * A source of media consisting of one or more {@link MediaPeriod}s. + * Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main + * responsibilities: + *
        + *
      • To provide the player with a {@link Timeline} defining the structure of its media, and to + * provide a new timeline whenever the structure of the media changes. The MediaSource provides + * these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener} + * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
      • + *
      • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the + * player to load and read the media.
      • + *
      + * All methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaSource { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index e76f0fd7e2..6cce902e87 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -69,6 +69,11 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the timing line. boolean haveEndTimecode = false; currentLine = subripData.readLine(); + if (currentLine == null) { + Log.w(TAG, "Unexpected end"); + break; + } + Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); if (matcher.matches()) { cueTimesUs.add(parseTimecode(matcher, 1)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 30c9c8737e..54af4dbf63 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -21,6 +21,7 @@ import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -92,19 +93,24 @@ import java.util.regex.Pattern; /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); + if (firstLine == null) { + return false; + } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); - } else { - // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); - cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); - if (cueHeaderMatcher.matches()) { - // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); - } + } + // The first line is not the timestamps, but could be the cue id. + String secondLine = webvttData.readLine(); + if (secondLine == null) { + return false; + } + cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); + if (cueHeaderMatcher.matches()) { + // We can do the rest of the parsing, including the id. + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, + styles); } return false; } @@ -233,7 +239,7 @@ import java.util.regex.Pattern; // Parse the cue text. textBuilder.setLength(0); String line; - while ((line = webvttData.readLine()) != null && !line.isEmpty()) { + while (!TextUtils.isEmpty(line = webvttData.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index fe2b920933..ba0f63b0bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -767,7 +767,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) { + Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) + throws ExoPlaybackException { int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; @@ -893,7 +894,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params) { + Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -960,7 +961,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, Parameters params) { + int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 45ac9eab6e..d518b5a6be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -199,6 +199,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param trackIndex The index of the track within the track group. * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. */ @@ -214,6 +215,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns @@ -615,12 +617,12 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. *

      - * A {@link TrackGroup} is mapped to the renderer that reports - * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group, - * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In - * the case that two or more renderers report the same level of support, the renderer with the - * lowest index is associated. + * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in + * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers + * report the same level of support, the renderer with the lowest index is associated. *

      * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the * tracks in the group, then {@code renderers.length} is returned to indicate that the group was diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index d118b91378..c37599eccc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -76,8 +76,8 @@ public final class ContentDataSource implements DataSource { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - long assertStartOffset = assetFileDescriptor.getStartOffset(); - long skipped = inputStream.skip(assertStartOffset + dataSpec.position) - assertStartOffset; + long assetStartOffset = assetFileDescriptor.getStartOffset(); + long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to // skip beyond the end of the data. @@ -86,8 +86,8 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = assetFileDescriptor.getLength(); - if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) { + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) { // The asset must extend to the end of the file. bytesRemaining = inputStream.available(); if (bytesRemaining == 0) { @@ -96,6 +96,8 @@ public final class ContentDataSource implements DataSource { // case, so treat as unbounded. bytesRemaining = C.LENGTH_UNSET; } + } else { + bytesRemaining = assetFileDescriptorLength - skipped; } } } catch (IOException e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 58cc70d68d..809f15b5a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -64,6 +64,7 @@ import javax.crypto.spec.SecretKeySpec; private final AtomicFile atomicFile; private final Cipher cipher; private final SecretKeySpec secretKeySpec; + private final boolean encrypt; private boolean changed; private ReusableBufferedOutputStream bufferedOutputStream; @@ -80,10 +81,21 @@ import javax.crypto.spec.SecretKeySpec; * Creates a CachedContentIndex which works on the index file in the given cacheDir. * * @param cacheDir Directory where the index file is kept. - * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. - * The key must be 16 bytes long. + * @param secretKey 16 byte AES key for reading and writing the cache index. */ public CachedContentIndex(File cacheDir, byte[] secretKey) { + this(cacheDir, secretKey, secretKey != null); + } + + /** + * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * + * @param cacheDir Directory where the index file is kept. + * @param secretKey 16 byte AES key for reading, and optionally writing, the cache index. + * @param encrypt When false, a plaintext index will be written. + */ + public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) { + this.encrypt = encrypt; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { @@ -288,10 +300,11 @@ import javax.crypto.spec.SecretKeySpec; output = new DataOutputStream(bufferedOutputStream); output.writeInt(VERSION); - int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0; + boolean writeEncrypted = encrypt && cipher != null; + int flags = writeEncrypted ? FLAG_ENCRYPTED_INDEX : 0; output.writeInt(flags); - if (cipher != null) { + if (writeEncrypted) { byte[] initializationVector = new byte[16]; new Random().nextBytes(initializationVector); output.write(initializationVector); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 2da6ba759b..15a5673a4e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -61,10 +61,24 @@ public final class SimpleCache implements Cache { * The key must be 16 bytes long. */ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) { + this(cacheDir, evictor, secretKey, secretKey != null); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. + * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. + * The key must be 16 bytes long. + * @param encrypt When false, a plaintext index will be written. + */ + public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir, secretKey); + this.index = new CachedContentIndex(cacheDir, secretKey, encrypt); this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 2d4a1ec96f..2daf16d3d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -85,6 +85,7 @@ public final class MimeTypes { public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; + public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; private MimeTypes() {} @@ -184,9 +185,9 @@ public final class MimeTypes { return MimeTypes.VIDEO_H264; } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { return MimeTypes.VIDEO_H265; - } else if (codec.startsWith("vp9")) { + } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) { return MimeTypes.VIDEO_VP9; - } else if (codec.startsWith("vp8")) { + } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { return MimeTypes.AUDIO_AAC; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 2a907e5955..70cb584085 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -428,7 +429,7 @@ public final class ParsableByteArray { * @return The string encoded by the bytes. */ public String readString(int length) { - return readString(length, Charset.defaultCharset()); + return readString(length, Charset.forName(C.UTF8_NAME)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index b958a54244..519919f129 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -253,7 +253,7 @@ public final class Util { * @return The code points encoding using UTF-8. */ public static byte[] getUtf8Bytes(String value) { - return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. + return value.getBytes(Charset.forName(C.UTF8_NAME)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index e32f23fed7..450b4af38c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -34,6 +34,8 @@ import static android.opengl.EGL14.EGL_WINDOW_BIT; import static android.opengl.EGL14.eglChooseConfig; import static android.opengl.EGL14.eglCreateContext; import static android.opengl.EGL14.eglCreatePbufferSurface; +import static android.opengl.EGL14.eglDestroyContext; +import static android.opengl.EGL14.eglDestroySurface; import static android.opengl.EGL14.eglGetDisplay; import static android.opengl.EGL14.eglInitialize; import static android.opengl.EGL14.eglMakeCurrent; @@ -89,12 +91,7 @@ public final class DummySurface extends Surface { */ public static synchronized boolean isSecureSupported(Context context) { if (!secureSupportedInitialized) { - if (Util.SDK_INT >= 17) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content") - && !deviceNeedsSecureDummySurfaceWorkaround(context); - } + secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); secureSupportedInitialized = true; } return secureSupported; @@ -147,20 +144,28 @@ public final class DummySurface extends Surface { } /** - * Returns whether the device is known to advertise secure surface textures but not implement them - * correctly. + * Returns whether use of secure dummy surfaces should be enabled. * * @param context Any {@link Context}. */ - private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 - && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")) - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); - } - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + private static boolean enableSecureDummySurfaceV24(Context context) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { + // EGL_EXT_protected_content is required to enable secure dummy surfaces. + return false; + } + if (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) { + // Samsung devices running API level 24 are known to be broken [Internal: b/37197802]. + return false; + } + if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + return true; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, @@ -171,6 +176,9 @@ public final class DummySurface extends Surface { private static final int MSG_RELEASE = 3; private final int[] textureIdHolder; + private EGLDisplay display; + private EGLContext context; + private EGLSurface pbuffer; private Handler handler; private SurfaceTexture surfaceTexture; @@ -255,7 +263,7 @@ public final class DummySurface extends Surface { } private void initInternal(boolean secure) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); int[] version = new int[2]; @@ -292,8 +300,8 @@ public final class DummySurface extends Surface { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; } - EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, - glAttributes, 0); + context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, + 0); Assertions.checkState(context != null, "eglCreateContext failed"); int[] pbufferAttributes; @@ -309,7 +317,7 @@ public final class DummySurface extends Surface { EGL_HEIGHT, 1, EGL_NONE}; } - EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); @@ -323,11 +331,22 @@ public final class DummySurface extends Surface { private void releaseInternal() { try { - surfaceTexture.release(); + if (surfaceTexture != null) { + surfaceTexture.release(); + glDeleteTextures(1, textureIdHolder, 0); + } } finally { + if (pbuffer != null) { + eglDestroySurface(display, pbuffer); + } + if (context != null) { + eglDestroyContext(display, context); + } + pbuffer = null; + context = null; + display = null; surface = null; surfaceTexture = null; - glDeleteTextures(1, textureIdHolder, 0); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 9a2927cc3f..9d769b2050 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -77,6 +77,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private Format[] streamFormats; private CodecMaxValues codecMaxValues; + private boolean codecNeedsSetOutputSurfaceWorkaround; private Surface surface; private Surface dummySurface; @@ -354,7 +355,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { MediaCodec codec = getCodec(); - if (Util.SDK_INT >= 23 && codec != null && surface != null) { + if (Util.SDK_INT >= 23 && codec != null && surface != null + && !codecNeedsSetOutputSurfaceWorkaround) { setOutputSurfaceV23(codec, surface); } else { releaseCodec(); @@ -425,6 +427,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); } @Override @@ -735,28 +738,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -30000; } - @SuppressLint("InlinedApi") - private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. - if (codecMaxValues.inputSize != Format.NO_VALUE) { - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); - } - // Set FRC workaround. - if (deviceNeedsAutoFrcWorkaround) { - frameworkMediaFormat.setInteger("auto-frc", 0); - } - // Configure tunneling if enabled. - if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); - } - return frameworkMediaFormat; - } - @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); @@ -812,6 +793,40 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder when + * playing media in the specified input format. + * + * @param format The format of input media. + * @param codecMaxValues The codec's maximum supported values. + * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion + * logic that negatively impacts ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, + boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { + MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); + // Set the maximum adaptive video dimensions. + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + // Set the maximum input size. + if (codecMaxValues.inputSize != Format.NO_VALUE) { + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + } + // Set FRC workaround. + if (deviceNeedsAutoFrcWorkaround) { + frameworkMediaFormat.setInteger("auto-frc", 0); + } + // Configure tunneling if enabled. + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); + } + return frameworkMediaFormat; + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats that are expected to have the @@ -951,6 +966,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } + /** + * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + *

      + * If true is returned then we fall back to releasing and re-instantiating the codec instead. + */ + private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + // Work around https://github.com/google/ExoPlayer/issues/3236 + return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + && "OMX.qcom.video.decoder.avc".equals(name); + } + /** * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between * two {@link Format}s. diff --git a/library/dash/README.md b/library/dash/README.md new file mode 100644 index 0000000000..394a38a332 --- /dev/null +++ b/library/dash/README.md @@ -0,0 +1,12 @@ +# ExoPlayer DASH library module # + +Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To +play DASH content, instantiate a `DashMediaSource` and pass it to +`ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 297052f65a..dd62d47621 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -85,14 +85,14 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int[] adaptationSetIndices; private final TrackSelection trackSelection; private final int trackType; - private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; private final int maxSegmentsPerLoad; + protected final RepresentationHolder[] representationHolders; + private DashManifest manifest; private int periodIndex; - private IOException fatalError; private boolean missingLastSegment; @@ -377,9 +377,12 @@ public class DefaultDashChunkSource implements DashChunkSource { // Protected classes. + /** + * Holds information about a single {@link Representation}. + */ protected static final class RepresentationHolder { - public final ChunkExtractorWrapper extractorWrapper; + /* package */ final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; @@ -387,7 +390,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation, + /* package */ RepresentationHolder(long periodDurationUs, Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; @@ -417,8 +420,8 @@ public class DefaultDashChunkSource implements DashChunkSource { segmentIndex = representation.getIndex(); } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) - throws BehindLiveWindowException{ + /* package */ void updateRepresentation(long newPeriodDurationUs, + Representation newRepresentation) throws BehindLiveWindowException { DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex(); diff --git a/library/hls/README.md b/library/hls/README.md new file mode 100644 index 0000000000..6f7e9d08d9 --- /dev/null +++ b/library/hls/README.md @@ -0,0 +1,11 @@ +# ExoPlayer HLS library module # + +Provides support for HTTP Live Streaming (HLS) content. To play HLS content, +instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index bca62ed230..7173d0d6d5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -92,6 +92,7 @@ import java.util.List; private byte[] scratchSpace; private IOException fatalError; private HlsUrl expectedPlaylistUrl; + private boolean independentSegments; private Uri encryptionKeyUri; private byte[] encryptionKey; @@ -206,10 +207,11 @@ import java.util.List; int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); expectedPlaylistUrl = null; - // Use start time of the previous chunk rather than its end time because switching format will - // require downloading overlapping segments. - long bufferedDurationUs = previous == null ? 0 - : Math.max(0, previous.startTimeUs - playbackPositionUs); + // Unless segments are known to be independent, switching variant will require downloading + // overlapping segments. Hence we use the start time of the previous chunk rather than its end + // time for this case. + long bufferedDurationUs = previous == null ? 0 : Math.max(0, + (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); @@ -224,12 +226,13 @@ import java.util.List; return; } HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + independentSegments = mediaPlaylist.hasIndependentSegmentsTag; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { long targetPositionUs = previous == null ? playbackPositionUs - : mediaPlaylist.hasIndependentSegmentsTag ? previous.endTimeUs : previous.startTimeUs; + : independentSegments ? previous.endTimeUs : previous.startTimeUs; if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md new file mode 100644 index 0000000000..69265e8702 --- /dev/null +++ b/library/smoothstreaming/README.md @@ -0,0 +1,12 @@ +# ExoPlayer SmoothStreaming library module # + +Provides support for Smooth Streaming content. To play Smooth Streaming content, +instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching + `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this + module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md new file mode 100644 index 0000000000..34e93e43af --- /dev/null +++ b/library/ui/README.md @@ -0,0 +1,10 @@ +# ExoPlayer UI library module # + +Provides UI components and resources for use with ExoPlayer. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index b0df16b484..037519b7a4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,8 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, + RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -51,6 +52,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { * The specified aspect ratio is ignored. */ public static final int RESIZE_MODE_FILL = 3; + /** + * Either the width or height is increased to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_ZOOM = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -85,7 +90,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Set the aspect ratio that this view should satisfy. + * Sets the aspect ratio that this view should satisfy. * * @param widthHeightRatio The width to height ratio. */ @@ -96,6 +101,13 @@ public final class AspectRatioFrameLayout extends FrameLayout { } } + /** + * Returns the resize mode. + */ + public @ResizeMode int getResizeMode() { + return resizeMode; + } + /** * Sets the resize mode. * @@ -132,6 +144,13 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; + case RESIZE_MODE_ZOOM: + if (aspectDeformation > 0) { + width = (int) (height * videoAspectRatio); + } else { + height = (int) (width / videoAspectRatio); + } + break; default: if (aspectDeformation > 0) { height = (int) (width / videoAspectRatio); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 6ddbfed973..123b3051e5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -312,6 +313,8 @@ public class PlaybackControlView extends FrameLayout { private long hideAtMs; private long[] adGroupTimesMs; private boolean[] playedAdGroups; + private long[] extraAdGroupTimesMs; + private boolean[] extraPlayedAdGroups; private final Runnable updateProgressAction = new Runnable() { @Override @@ -336,15 +339,19 @@ public class PlaybackControlView extends FrameLayout { } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, attrs); + } + public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr, + AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; - if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + if (playbackAttrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); try { rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); @@ -364,6 +371,8 @@ public class PlaybackControlView extends FrameLayout { formatter = new Formatter(formatBuilder, Locale.getDefault()); adGroupTimesMs = new long[0]; playedAdGroups = new boolean[0]; + extraAdGroupTimesMs = new long[0]; + extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); controlDispatcher = DEFAULT_CONTROL_DISPATCHER; @@ -462,6 +471,29 @@ public class PlaybackControlView extends FrameLayout { updateTimeBarMode(); } + /** + * Sets the millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs, + @Nullable boolean[] extraPlayedAdGroups) { + if (extraAdGroupTimesMs == null) { + this.extraAdGroupTimesMs = new long[0]; + this.extraPlayedAdGroups = new boolean[0]; + } else { + Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); + this.extraAdGroupTimesMs = extraAdGroupTimesMs; + this.extraPlayedAdGroups = extraPlayedAdGroups; + } + updateProgress(); + } + /** * Sets the {@link VisibilityListener}. * @@ -647,9 +679,10 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode()) - || isSeekable || !window.isDynamic; - enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic; + enablePrevious = isSeekable || !window.isDynamic + || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + enableNext = window.isDynamic + || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); @@ -768,7 +801,15 @@ public class PlaybackControlView extends FrameLayout { bufferedPosition += player.getBufferedPosition(); } if (timeBar != null) { - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, adGroupCount); + int extraAdGroupCount = extraAdGroupTimesMs.length; + int totalAdGroupCount = adGroupCount + extraAdGroupCount; + if (totalAdGroupCount > adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); + playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); + } + System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); + System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); } } if (durationView != null) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 2bba9071fd..a4fb539175 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -239,7 +239,7 @@ public final class SimpleExoPlayerView extends FrameLayout { controller = null; componentListener = null; overlayFrameLayout = null; - ImageView logo = new ImageView(context, attrs); + ImageView logo = new ImageView(context); if (Util.SDK_INT >= 23) { configureEditModeLogoV23(getResources(), logo); } else { @@ -329,9 +329,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (customController != null) { this.controller = customController; } else if (controllerPlaceholder != null) { - // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit - // calls to set them. - this.controller = new PlaybackControlView(context, attrs); + // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlaybackControlView(context, null, 0, attrs); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); @@ -379,9 +379,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and - * {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous - * assignments are overridden. + * Set the {@link SimpleExoPlayer} to use. *

      * To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to * use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} rather @@ -397,8 +395,8 @@ public final class SimpleExoPlayerView extends FrameLayout { } if (this.player != null) { this.player.removeListener(componentListener); - this.player.clearTextOutput(componentListener); - this.player.clearVideoListener(componentListener); + this.player.removeTextOutput(componentListener); + this.player.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { this.player.clearVideoTextureView((TextureView) surfaceView); } else if (surfaceView instanceof SurfaceView) { @@ -418,8 +416,8 @@ public final class SimpleExoPlayerView extends FrameLayout { } else if (surfaceView instanceof SurfaceView) { player.setVideoSurfaceView((SurfaceView) surfaceView); } - player.setVideoListener(componentListener); - player.setTextOutput(componentListener); + player.addVideoListener(componentListener); + player.addTextOutput(componentListener); player.addListener(componentListener); maybeShowController(false); updateForCurrentTrackSelections(); @@ -429,6 +427,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160 + surfaceView.setVisibility(visibility); + } + } + /** * Sets the resize mode. * @@ -668,10 +675,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * Gets the view onto which video is rendered. This is either a {@link SurfaceView} (default) - * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. + * Gets the view onto which video is rendered. This is a: + *

        + *
      • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to + * {@code surface_view}.
      • + *
      • {@link TextureView} if {@code surface_type} is {@code texture_view}.
      • + *
      • {@code null} if {@code surface_type} is {@code none}.
      • + *
      * - * @return Either a {@link SurfaceView} or a {@link TextureView}. + * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. */ public View getVideoSurfaceView() { return surfaceView; 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 5819a4b711..2e59b33c0b 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 @@ -181,7 +181,7 @@ public class TestUtil { byte[] expectedData) throws IOException { try { long length = dataSource.open(dataSpec); - Assert.assertEquals(length, expectedData.length); + Assert.assertEquals(expectedData.length, length); byte[] readData = TestUtil.readToEnd(dataSource); MoreAsserts.assertEquals(expectedData, readData); } finally {