3
.gitignore
vendored
@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx
|
||||
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||
extensions/vp9/src/main/jni/libyuv
|
||||
|
||||
# AV1 extension
|
||||
extensions/av1/src/main/jni/libgav1
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
||||
|
@ -62,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx
|
||||
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||
extensions/vp9/src/main/jni/libyuv
|
||||
|
||||
# AV1 extension
|
||||
extensions/av1/src/main/jni/libgav1
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
||||
|
173
RELEASENOTES.md
@ -1,5 +1,167 @@
|
||||
# Release notes #
|
||||
|
||||
### 2.11.0 (2019-12-11) ###
|
||||
|
||||
* Core library:
|
||||
* Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and
|
||||
`ExoPlayer.Builder`.
|
||||
* Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled
|
||||
by calling `SimpleExoPlayer.setHandleWakeLock`
|
||||
([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this
|
||||
feature, you must add the
|
||||
[WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK)
|
||||
permission to your application's manifest file.
|
||||
* Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which
|
||||
can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`.
|
||||
* Wrap decoder exceptions in a new `DecoderException` class and report them as
|
||||
renderer errors.
|
||||
* Add `Timeline.Window.isLive` to indicate that a window is a live stream
|
||||
([#2668](https://github.com/google/ExoPlayer/issues/2668) and
|
||||
[#5973](https://github.com/google/ExoPlayer/issues/5973)).
|
||||
* Add `Timeline.Window.uid` to uniquely identify window instances.
|
||||
* Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be
|
||||
set.
|
||||
* Deprecate passing the manifest directly to
|
||||
`Player.EventListener.onTimelineChanged`. It can be accessed through
|
||||
`Timeline.Window.manifest` or `Player.getCurrentManifest()`
|
||||
* Add `MediaSource.enable` and `MediaSource.disable` to improve resource
|
||||
management in playlists.
|
||||
* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state.
|
||||
* Fix issue where player errors are thrown too early at playlist transitions
|
||||
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
|
||||
* Add `Format` and renderer support flags to renderer `ExoPlaybackException`s.
|
||||
* DRM:
|
||||
* Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`.
|
||||
This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a
|
||||
different `DrmSessionManager`
|
||||
([#5619](https://github.com/google/ExoPlayer/issues/5619)).
|
||||
* Add `DefaultDrmSessionManager.Builder`, and remove
|
||||
`DefaultDrmSessionManager` static factory methods that leaked
|
||||
`ExoMediaDrm` instances
|
||||
([#4721](https://github.com/google/ExoPlayer/issues/4721)).
|
||||
* Add support for the use of secure decoders when playing clear content
|
||||
([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can
|
||||
be enabled using `DefaultDrmSessionManager.Builder`'s
|
||||
`setUseDrmSessionsForClearContent` method.
|
||||
* Add support for custom `LoadErrorHandlingPolicies` in key and provisioning
|
||||
requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom
|
||||
policies can be passed via `DefaultDrmSessionManager.Builder`'s
|
||||
`setLoadErrorHandlingPolicy` method.
|
||||
* Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking
|
||||
`ExoMediaDrm` instances
|
||||
([#4721](https://github.com/google/ExoPlayer/issues/4721)).
|
||||
* Track selection:
|
||||
* Update `DefaultTrackSelector` to set a viewport constraint for the default
|
||||
display by default.
|
||||
* Update `DefaultTrackSelector` to set text language and role flag
|
||||
constraints for the device's accessibility settings by default
|
||||
([#5749](https://github.com/google/ExoPlayer/issues/5749)).
|
||||
* Add option to set preferred text role flags using
|
||||
`DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`.
|
||||
* Android 10:
|
||||
* Set `compileSdkVersion` to 29 to enable use of Android 10 APIs.
|
||||
* Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags
|
||||
in `MediaCodecInfo`
|
||||
([#5839](https://github.com/google/ExoPlayer/issues/5839)).
|
||||
* Add `allowedCapturePolicy` field to `AudioAttributes` to allow to
|
||||
configuration of the audio capture policy.
|
||||
* Video:
|
||||
* Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`.
|
||||
* Fix byte order of HDR10+ static metadata to match CTA-861.3.
|
||||
* Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska.
|
||||
* Assume that protected content requires a secure decoder when evaluating
|
||||
whether `MediaCodecVideoRenderer` supports a given video format
|
||||
([#5568](https://github.com/google/ExoPlayer/issues/5568)).
|
||||
* Fix Dolby Vision fallback to AVC and HEVC.
|
||||
* Fix early end-of-stream detection when using video tunneling, on API level
|
||||
23 and above.
|
||||
* Fix an issue where a keyframe was rendered rather than skipped when
|
||||
performing an exact seek to a non-zero position close to the start of the
|
||||
stream.
|
||||
* Audio:
|
||||
* Fix the start of audio getting truncated when transitioning to a new
|
||||
item in a playlist of Opus streams.
|
||||
* Workaround broken raw audio decoding on Oppo R9
|
||||
([#5782](https://github.com/google/ExoPlayer/issues/5782)).
|
||||
* Reconfigure audio sink when PCM encoding changes
|
||||
([#6601](https://github.com/google/ExoPlayer/issues/6601)).
|
||||
* Allow `AdtsExtractor` to encounter EOF when calculating average frame size
|
||||
([#6700](https://github.com/google/ExoPlayer/issues/6700)).
|
||||
* Text:
|
||||
* Add support for position and overlapping start/end times in SSA/ASS
|
||||
subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)).
|
||||
* Require an end time or duration for SubRip (SRT) and SubStation Alpha
|
||||
(SSA/ASS) subtitles. This applies to both sidecar files & subtitles
|
||||
[embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html).
|
||||
* UI:
|
||||
* Make showing and hiding player controls accessible to TalkBack in
|
||||
`PlayerView`.
|
||||
* Rename `spherical_view` surface type to `spherical_gl_surface_view`.
|
||||
* Make it easier to override the shuffle, repeat, fullscreen, VR and small
|
||||
notification icon assets
|
||||
([#6709](https://github.com/google/ExoPlayer/issues/6709)).
|
||||
* Analytics:
|
||||
* Remove `AnalyticsCollector.Factory`. Instances should be created directly,
|
||||
and the `Player` should be set by calling `AnalyticsCollector.setPlayer`.
|
||||
* Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and
|
||||
analytics reporting.
|
||||
* DataSource
|
||||
* Add `DataSpec.httpRequestHeaders` to support setting per-request headers for
|
||||
HTTP and HTTPS.
|
||||
* Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by
|
||||
setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in
|
||||
`DataSpec.httpRequestHeaders`.
|
||||
* Fail more explicitly when local file URIs contain invalid parts (e.g. a
|
||||
fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)).
|
||||
* DASH: Support negative @r values in segment timelines
|
||||
([#1787](https://github.com/google/ExoPlayer/issues/1787)).
|
||||
* HLS:
|
||||
* Use peak bitrate rather than average bitrate for adaptive track selection.
|
||||
* Fix issue where streams could get stuck in an infinite buffering state
|
||||
after a postroll ad
|
||||
([#6314](https://github.com/google/ExoPlayer/issues/6314)).
|
||||
* Matroska: Support lacing in Blocks
|
||||
([#3026](https://github.com/google/ExoPlayer/issues/3026)).
|
||||
* AV1 extension:
|
||||
* New in this release. The AV1 extension allows use of the
|
||||
[libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/)
|
||||
in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer
|
||||
[here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9).
|
||||
* VP9 extension:
|
||||
* Update to use NDK r20.
|
||||
* Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the
|
||||
core library.
|
||||
* Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to
|
||||
`C.MSG_SET_OUTPUT_BUFFER_RENDERER`.
|
||||
* Use `VideoDecoderRenderer` as an implementation of
|
||||
`VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`.
|
||||
* Flac extension: Update to use NDK r20.
|
||||
* Opus extension: Update to use NDK r20.
|
||||
* FFmpeg extension:
|
||||
* Update to use NDK r20.
|
||||
* Update to use FFmpeg version 4.2. It is necessary to rebuild the native part
|
||||
of the extension after this change, following the instructions in the
|
||||
extension's readme.
|
||||
* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to
|
||||
support `ACTION_SET_CAPTIONING_ENABLED` events.
|
||||
* GVR extension: This extension is now deprecated.
|
||||
* Demo apps:
|
||||
* Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface)
|
||||
to show how to use the Android 10 `SurfaceControl` API with ExoPlayer
|
||||
([#677](https://github.com/google/ExoPlayer/issues/677)).
|
||||
* Add support for subtitle files to the
|
||||
[Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main)
|
||||
([#5523](https://github.com/google/ExoPlayer/issues/5523)).
|
||||
* Remove the IMA demo app. IMA functionality is demonstrated by the
|
||||
[main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main).
|
||||
* Add basic DRM support to the
|
||||
[Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast).
|
||||
* TestUtils: Publish the `testutils` module to simplify unit testing with
|
||||
ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)).
|
||||
* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an
|
||||
`AdEventListener` provided by the app
|
||||
([#6687](https://github.com/google/ExoPlayer/issues/6687)).
|
||||
|
||||
### 2.10.8 (2019-11-19) ###
|
||||
|
||||
* E-AC3 JOC
|
||||
@ -23,7 +185,7 @@
|
||||
* MediaSession extension: Update shuffle and repeat modes when playback state
|
||||
is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)).
|
||||
* Fix the start of audio getting truncated when transitioning to a new
|
||||
item in a playlist of opus streams.
|
||||
item in a playlist of Opus streams.
|
||||
|
||||
### 2.10.6 (2019-10-17) ###
|
||||
|
||||
@ -264,6 +426,7 @@
|
||||
* Update `TrackSelection.Factory` interface to support creating all track
|
||||
selections together.
|
||||
* Allow to specify a selection reason for a `SelectionOverride`.
|
||||
* Select audio track based on system language if no preference is provided.
|
||||
* When no text language preference matches, only select forced text tracks
|
||||
whose language matches the selected audio language.
|
||||
* UI:
|
||||
@ -592,7 +755,7 @@
|
||||
and `AnalyticsListener` callbacks
|
||||
([#4361](https://github.com/google/ExoPlayer/issues/4361) and
|
||||
[#4615](https://github.com/google/ExoPlayer/issues/4615)).
|
||||
* UI components:
|
||||
* UI:
|
||||
* Add option to `PlayerView` to show buffering view when playWhenReady is
|
||||
false ([#4304](https://github.com/google/ExoPlayer/issues/4304)).
|
||||
* Allow any `Drawable` to be used as `PlayerView` default artwork.
|
||||
@ -748,7 +911,7 @@
|
||||
* OkHttp extension: Fix to correctly include response headers in thrown
|
||||
`InvalidResponseCodeException`s.
|
||||
* Add possibility to cancel `PlayerMessage`s.
|
||||
* UI components:
|
||||
* UI:
|
||||
* Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed
|
||||
video frame or media artwork visible when the player is reset
|
||||
([#2843](https://github.com/google/ExoPlayer/issues/2843)).
|
||||
@ -798,7 +961,7 @@
|
||||
* Support live stream clipping with `ClippingMediaSource`.
|
||||
* Allow setting tags for all media sources in their factories. The tag of the
|
||||
current window can be retrieved with `Player.getCurrentTag`.
|
||||
* UI components:
|
||||
* UI:
|
||||
* Add support for displaying error messages and a buffering spinner in
|
||||
`PlayerView`.
|
||||
* Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
|
||||
@ -962,7 +1125,7 @@
|
||||
`SsMediaSource.Factory`, and `MergingMediaSource`.
|
||||
* Play out existing buffer before retrying for progressive live streams
|
||||
([#1606](https://github.com/google/ExoPlayer/issues/1606)).
|
||||
* UI components:
|
||||
* UI:
|
||||
* Generalized player and control views to allow them to bind with any
|
||||
`Player`, and renamed them to `PlayerView` and `PlayerControlView`
|
||||
respectively.
|
||||
|
@ -13,17 +13,30 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.10.8'
|
||||
releaseVersionCode = 2010008
|
||||
releaseVersion = '2.11.0'
|
||||
releaseVersionCode = 2011000
|
||||
minSdkVersion = 16
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 28
|
||||
appTargetSdkVersion = 29
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
|
||||
compileSdkVersion = 29
|
||||
dexmakerVersion = '2.21.0'
|
||||
guavaVersion = '23.5-android'
|
||||
mockitoVersion = '2.25.0'
|
||||
robolectricVersion = '4.2'
|
||||
robolectricVersion = '4.3'
|
||||
autoValueVersion = '1.6'
|
||||
autoServiceVersion = '1.0-rc4'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
androidXTestVersion = '1.1.0'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinAnnotationsVersion = '1.3.31'
|
||||
androidxAnnotationVersion = '1.1.0'
|
||||
androidxAppCompatVersion = '1.1.0'
|
||||
androidxCollectionVersion = '1.1.0'
|
||||
androidxMediaVersion = '1.0.1'
|
||||
androidxTestCoreVersion = '1.2.0'
|
||||
androidxTestJUnitVersion = '1.1.1'
|
||||
androidxTestRunnerVersion = '1.2.0'
|
||||
androidxTestRulesVersion = '1.2.0'
|
||||
truthVersion = '0.44'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
@ -24,7 +24,7 @@ include modulePrefix + 'library-hls'
|
||||
include modulePrefix + 'library-smoothstreaming'
|
||||
include modulePrefix + 'library-ui'
|
||||
include modulePrefix + 'testutils'
|
||||
include modulePrefix + 'testutils-robolectric'
|
||||
include modulePrefix + 'extension-av1'
|
||||
include modulePrefix + 'extension-ffmpeg'
|
||||
include modulePrefix + 'extension-flac'
|
||||
include modulePrefix + 'extension-gvr'
|
||||
@ -47,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl
|
||||
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
||||
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
||||
project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric')
|
||||
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
|
||||
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
||||
|
@ -26,7 +26,7 @@ android {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -56,10 +56,9 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cast')
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
@ -1,399 +0,0 @@
|
||||
/*
|
||||
* 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.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.EventListener;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
||||
/* package */ class DefaultReceiverPlayerManager
|
||||
implements PlayerManager, EventListener, SessionAvailabilityListener {
|
||||
|
||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||
|
||||
private final PlayerView localPlayerView;
|
||||
private final PlayerControlView castControlView;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<MediaItem> mediaQueue;
|
||||
private final Listener listener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
|
||||
/**
|
||||
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
*
|
||||
* @param listener A {@link Listener} for queue position changes.
|
||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||
* @param context A {@link Context}.
|
||||
* @param castContext The {@link CastContext}.
|
||||
*/
|
||||
public DefaultReceiverPlayerManager(
|
||||
Listener listener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
this.listener = listener;
|
||||
this.localPlayerView = localPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
castPlayer = new CastPlayer(castContext);
|
||||
castPlayer.addListener(this);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
// Queue manipulation methods.
|
||||
|
||||
/**
|
||||
* Plays a specified queue item in the current player.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
*/
|
||||
@Override
|
||||
public void selectQueueItem(int itemIndex) {
|
||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||
}
|
||||
|
||||
/** Returns the index of the currently played item. */
|
||||
@Override
|
||||
public int getCurrentItemIndex() {
|
||||
return currentItemIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends {@code item} to the media queue.
|
||||
*
|
||||
* @param item The {@link MediaItem} to append.
|
||||
*/
|
||||
@Override
|
||||
public void addItem(MediaItem item) {
|
||||
mediaQueue.add(item);
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||
if (currentPlayer == castPlayer) {
|
||||
castPlayer.addItems(buildMediaQueueItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the size of the media queue. */
|
||||
@Override
|
||||
public int getMediaQueueSize() {
|
||||
return mediaQueue.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item at the given index in the media queue.
|
||||
*
|
||||
* @param position The index of the item.
|
||||
* @return The item at the given index in the media queue.
|
||||
*/
|
||||
@Override
|
||||
public MediaItem getItem(int position) {
|
||||
return mediaQueue.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item at the given index from the media queue.
|
||||
*
|
||||
* @param item The item to remove.
|
||||
* @return Whether the removal was successful.
|
||||
*/
|
||||
@Override
|
||||
public boolean removeItem(MediaItem item) {
|
||||
int itemIndex = mediaQueue.indexOf(item);
|
||||
if (itemIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
if (currentPlayer == castPlayer) {
|
||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||
return false;
|
||||
}
|
||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
||||
}
|
||||
}
|
||||
mediaQueue.remove(itemIndex);
|
||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||
} else if (itemIndex < currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item within the queue.
|
||||
*
|
||||
* @param item The item to move.
|
||||
* @param toIndex The target index of the item in the queue.
|
||||
* @return Whether the item move was successful.
|
||||
*/
|
||||
@Override
|
||||
public boolean moveItem(MediaItem item, int toIndex) {
|
||||
int fromIndex = mediaQueue.indexOf(item);
|
||||
if (fromIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
// Player update.
|
||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
int periodCount = castTimeline.getPeriodCount();
|
||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||
return false;
|
||||
}
|
||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
||||
castPlayer.moveItem(elementId, toIndex);
|
||||
}
|
||||
|
||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
||||
|
||||
// Index update.
|
||||
if (fromIndex == currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(toIndex);
|
||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||
*
|
||||
* @param event The {@link KeyEvent}.
|
||||
* @return Whether the event was handled by the target view.
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (currentPlayer == exoPlayer) {
|
||||
return localPlayerView.dispatchKeyEvent(event);
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
return castControlView.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/** Releases the manager and the players that it holds. */
|
||||
@Override
|
||||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
concatenatingMediaSource.clear();
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
localPlayerView.setPlayer(null);
|
||||
exoPlayer.release();
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
||||
@Override
|
||||
public void onCastSessionAvailable() {
|
||||
setCurrentPlayer(castPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCastSessionUnavailable() {
|
||||
setCurrentPlayer(exoPlayer);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void updateCurrentItemIndex() {
|
||||
int playbackState = currentPlayer.getPlaybackState();
|
||||
maybeSetCurrentItemAndNotify(
|
||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||
? currentPlayer.getCurrentWindowIndex()
|
||||
: C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
private void setCurrentPlayer(Player currentPlayer) {
|
||||
if (this.currentPlayer == currentPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// View management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
localPlayerView.setVisibility(View.VISIBLE);
|
||||
castControlView.hide();
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
localPlayerView.setVisibility(View.GONE);
|
||||
castControlView.show();
|
||||
}
|
||||
|
||||
// Player state management.
|
||||
long playbackPositionMs = C.TIME_UNSET;
|
||||
int windowIndex = C.INDEX_UNSET;
|
||||
boolean playWhenReady = false;
|
||||
if (this.currentPlayer != null) {
|
||||
int playbackState = this.currentPlayer.getPlaybackState();
|
||||
if (playbackState != Player.STATE_ENDED) {
|
||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
||||
if (windowIndex != currentItemIndex) {
|
||||
playbackPositionMs = C.TIME_UNSET;
|
||||
windowIndex = currentItemIndex;
|
||||
}
|
||||
}
|
||||
this.currentPlayer.stop(true);
|
||||
} else {
|
||||
// This is the initial setup. No need to save any state.
|
||||
}
|
||||
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
|
||||
// Playback transition.
|
||||
if (windowIndex != C.INDEX_UNSET) {
|
||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the item at the given position.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
* @param positionMs The position at which playback should start.
|
||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||
*/
|
||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||
if (this.currentItemIndex != currentItemIndex) {
|
||||
int oldIndex = this.currentItemIndex;
|
||||
this.currentItemIndex = currentItemIndex;
|
||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaSource buildMediaSource(MediaItem item) {
|
||||
Uri uri = item.media.uri;
|
||||
switch (item.mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
default:
|
||||
{
|
||||
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaQueueItem buildMediaQueueItem(MediaItem item) {
|
||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
||||
MediaInfo mediaInfo =
|
||||
new MediaInfo.Builder(item.media.uri.toString())
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setContentType(item.mimeType)
|
||||
.setMetadata(movieMetadata)
|
||||
.build();
|
||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
||||
}
|
||||
}
|
@ -16,87 +16,86 @@
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Utility methods and constants for the Cast demo application. */
|
||||
/* package */ final class DemoUtil {
|
||||
|
||||
/** Represents a media sample. */
|
||||
public static final class Sample {
|
||||
|
||||
/** The uri of the media content. */
|
||||
public final String uri;
|
||||
/** The name of the sample. */
|
||||
public final String name;
|
||||
/** The mime type of the sample media content. */
|
||||
public final String mimeType;
|
||||
/**
|
||||
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
|
||||
* DRM-protected.
|
||||
*/
|
||||
@Nullable public final UUID drmSchemeUuid;
|
||||
/**
|
||||
* The url from which players should obtain DRM licenses, or null if the content is not
|
||||
* DRM-protected.
|
||||
*/
|
||||
@Nullable public final Uri licenseServerUri;
|
||||
|
||||
/**
|
||||
* @param uri See {@link #uri}.
|
||||
* @param name See {@link #name}.
|
||||
* @param mimeType See {@link #mimeType}.
|
||||
*/
|
||||
public Sample(String uri, String name, String mimeType) {
|
||||
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
|
||||
}
|
||||
|
||||
public Sample(
|
||||
String uri,
|
||||
String name,
|
||||
String mimeType,
|
||||
@Nullable UUID drmSchemeUuid,
|
||||
@Nullable String licenseServerUriString) {
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
this.drmSchemeUuid = drmSchemeUuid;
|
||||
this.licenseServerUri =
|
||||
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
||||
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
|
||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||
|
||||
/** The list of samples available in the cast demo app. */
|
||||
public static final List<Sample> SAMPLES;
|
||||
public static final List<MediaItem> SAMPLES;
|
||||
|
||||
static {
|
||||
// App samples.
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
ArrayList<MediaItem> samples = new ArrayList<>();
|
||||
|
||||
// Clear content.
|
||||
samples.add(
|
||||
new Sample(
|
||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
"Clear DASH: Tears",
|
||||
MIME_TYPE_DASH));
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
||||
.setTitle("Clear DASH: Tears")
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.build());
|
||||
samples.add(
|
||||
new Sample(
|
||||
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
|
||||
.setTitle("Clear HLS: Angel one")
|
||||
.setMimeType(MIME_TYPE_HLS)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://html5demos.com/assets/dizzy.mp4")
|
||||
.setTitle("Clear MP4: Dizzy")
|
||||
.setMimeType(MIME_TYPE_VIDEO_MP4)
|
||||
.build());
|
||||
|
||||
// DRM content.
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
|
||||
.setTitle("Widevine DASH cenc: Tears")
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(
|
||||
Uri.parse(
|
||||
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
|
||||
.setTitle("Widevine DASH cbc1: Tears")
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(
|
||||
Uri.parse(
|
||||
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
|
||||
.setTitle("Widevine DASH cbcs: Tears")
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
.build());
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
}
|
||||
|
@ -17,13 +17,6 @@ package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@ -34,16 +27,23 @@ import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.dynamite.DynamiteModule;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||
@ -52,8 +52,6 @@ import java.util.Collections;
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlayerManager.Listener {
|
||||
|
||||
private final MediaItem.Builder mediaItemBuilder;
|
||||
|
||||
private PlayerView localPlayerView;
|
||||
private PlayerControlView castControlView;
|
||||
private PlayerManager playerManager;
|
||||
@ -61,10 +59,6 @@ public class MainActivity extends AppCompatActivity
|
||||
private MediaQueueListAdapter mediaQueueListAdapter;
|
||||
private CastContext castContext;
|
||||
|
||||
public MainActivity() {
|
||||
mediaItemBuilder = new MediaItem.Builder();
|
||||
}
|
||||
|
||||
// Activity lifecycle methods.
|
||||
|
||||
@Override
|
||||
@ -118,20 +112,13 @@ public class MainActivity extends AppCompatActivity
|
||||
// There is no Cast context to work with. Do nothing.
|
||||
return;
|
||||
}
|
||||
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
|
||||
switch (applicationId) {
|
||||
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
||||
playerManager =
|
||||
new DefaultReceiverPlayerManager(
|
||||
/* listener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
castContext);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
|
||||
}
|
||||
playerManager =
|
||||
new PlayerManager(
|
||||
/* listener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
castContext);
|
||||
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
||||
}
|
||||
|
||||
@ -179,36 +166,29 @@ public class MainActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueContentsExternallyChanged() {
|
||||
mediaQueueListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError() {
|
||||
Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show();
|
||||
public void onUnsupportedTrack(int trackType) {
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
} else {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void showToast(int messageId) {
|
||||
Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private View buildSampleListView() {
|
||||
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
|
||||
ListView sampleList = dialogList.findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleListAdapter(this));
|
||||
sampleList.setOnItemClickListener(
|
||||
(parent, view, position, id) -> {
|
||||
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
|
||||
mediaItemBuilder
|
||||
.clear()
|
||||
.setMedia(sample.uri)
|
||||
.setTitle(sample.name)
|
||||
.setMimeType(sample.mimeType);
|
||||
if (sample.drmSchemeUuid != null) {
|
||||
mediaItemBuilder.setDrmSchemes(
|
||||
Collections.singletonList(
|
||||
new MediaItem.DrmScheme(
|
||||
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
|
||||
}
|
||||
playerManager.addItem(mediaItemBuilder.build());
|
||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||
});
|
||||
return dialogList;
|
||||
@ -231,8 +211,10 @@ public class MainActivity extends AppCompatActivity
|
||||
TextView view = holder.textView;
|
||||
view.setText(holder.item.title);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
view.setTextColor(
|
||||
ColorUtils.setAlphaComponent(
|
||||
view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -312,11 +294,18 @@ public class MainActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> {
|
||||
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
|
||||
|
||||
public SampleListAdapter(Context context) {
|
||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
((TextView) view).setText(getItem(position).title);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,49 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.EventListener;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
/** Manages the players in the Cast demo app. */
|
||||
/* package */ interface PlayerManager {
|
||||
/** Manages players and an internal media queue for the demo app. */
|
||||
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||
|
||||
/** Listener for events. */
|
||||
interface Listener {
|
||||
@ -28,40 +65,395 @@ import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
/** Called when the currently played item of the media queue changes. */
|
||||
void onQueuePositionChanged(int previousIndex, int newIndex);
|
||||
|
||||
/** Called when the media queue changes due to modifications not caused by this manager. */
|
||||
void onQueueContentsExternallyChanged();
|
||||
|
||||
/** Called when an error occurs in the current player. */
|
||||
void onPlayerError();
|
||||
/**
|
||||
* Called when a track of type {@code trackType} is not supported by the player.
|
||||
*
|
||||
* @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants.
|
||||
*/
|
||||
void onUnsupportedTrack(int trackType);
|
||||
}
|
||||
|
||||
/** Redirects the given {@code keyEvent} to the active player. */
|
||||
boolean dispatchKeyEvent(KeyEvent keyEvent);
|
||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||
|
||||
/** Appends the given {@link MediaItem} to the media queue. */
|
||||
void addItem(MediaItem mediaItem);
|
||||
private final PlayerView localPlayerView;
|
||||
private final PlayerControlView castControlView;
|
||||
private final DefaultTrackSelector trackSelector;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<MediaItem> mediaQueue;
|
||||
private final Listener listener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
private final MediaItemConverter mediaItemConverter;
|
||||
|
||||
/** Returns the number of items in the media queue. */
|
||||
int getMediaQueueSize();
|
||||
|
||||
/** Selects the item at the given position for playback. */
|
||||
void selectQueueItem(int position);
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
|
||||
/**
|
||||
* Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is
|
||||
* being played.
|
||||
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
*
|
||||
* @param listener A {@link Listener} for queue position changes.
|
||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||
* @param context A {@link Context}.
|
||||
* @param castContext The {@link CastContext}.
|
||||
*/
|
||||
int getCurrentItemIndex();
|
||||
public PlayerManager(
|
||||
Listener listener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
this.listener = listener;
|
||||
this.localPlayerView = localPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
mediaItemConverter = new DefaultMediaItemConverter();
|
||||
|
||||
/** Returns the {@link MediaItem} at the given {@code position}. */
|
||||
MediaItem getItem(int position);
|
||||
trackSelector = new DefaultTrackSelector(context);
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
/** Moves the item at position {@code from} to position {@code to}. */
|
||||
boolean moveItem(MediaItem item, int to);
|
||||
castPlayer = new CastPlayer(castContext);
|
||||
castPlayer.addListener(this);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
/** Removes the item at position {@code index}. */
|
||||
boolean removeItem(MediaItem item);
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
/** Releases any acquired resources. */
|
||||
void release();
|
||||
// Queue manipulation methods.
|
||||
|
||||
/**
|
||||
* Plays a specified queue item in the current player.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
*/
|
||||
public void selectQueueItem(int itemIndex) {
|
||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||
}
|
||||
|
||||
/** Returns the index of the currently played item. */
|
||||
public int getCurrentItemIndex() {
|
||||
return currentItemIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends {@code item} to the media queue.
|
||||
*
|
||||
* @param item The {@link MediaItem} to append.
|
||||
*/
|
||||
public void addItem(MediaItem item) {
|
||||
mediaQueue.add(item);
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||
if (currentPlayer == castPlayer) {
|
||||
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the size of the media queue. */
|
||||
public int getMediaQueueSize() {
|
||||
return mediaQueue.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item at the given index in the media queue.
|
||||
*
|
||||
* @param position The index of the item.
|
||||
* @return The item at the given index in the media queue.
|
||||
*/
|
||||
public MediaItem getItem(int position) {
|
||||
return mediaQueue.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item at the given index from the media queue.
|
||||
*
|
||||
* @param item The item to remove.
|
||||
* @return Whether the removal was successful.
|
||||
*/
|
||||
public boolean removeItem(MediaItem item) {
|
||||
int itemIndex = mediaQueue.indexOf(item);
|
||||
if (itemIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
if (currentPlayer == castPlayer) {
|
||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||
return false;
|
||||
}
|
||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
||||
}
|
||||
}
|
||||
mediaQueue.remove(itemIndex);
|
||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||
} else if (itemIndex < currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item within the queue.
|
||||
*
|
||||
* @param item The item to move.
|
||||
* @param toIndex The target index of the item in the queue.
|
||||
* @return Whether the item move was successful.
|
||||
*/
|
||||
public boolean moveItem(MediaItem item, int toIndex) {
|
||||
int fromIndex = mediaQueue.indexOf(item);
|
||||
if (fromIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
// Player update.
|
||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
int periodCount = castTimeline.getPeriodCount();
|
||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||
return false;
|
||||
}
|
||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
||||
castPlayer.moveItem(elementId, toIndex);
|
||||
}
|
||||
|
||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
||||
|
||||
// Index update.
|
||||
if (fromIndex == currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(toIndex);
|
||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||
*
|
||||
* @param event The {@link KeyEvent}.
|
||||
* @return Whether the event was handled by the target view.
|
||||
*/
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (currentPlayer == exoPlayer) {
|
||||
return localPlayerView.dispatchKeyEvent(event);
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
return castControlView.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/** Releases the manager and the players that it holds. */
|
||||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
concatenatingMediaSource.clear();
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
localPlayerView.setPlayer(null);
|
||||
exoPlayer.release();
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
|
||||
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
|
||||
trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
|
||||
}
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
}
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
||||
@Override
|
||||
public void onCastSessionAvailable() {
|
||||
setCurrentPlayer(castPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCastSessionUnavailable() {
|
||||
setCurrentPlayer(exoPlayer);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void updateCurrentItemIndex() {
|
||||
int playbackState = currentPlayer.getPlaybackState();
|
||||
maybeSetCurrentItemAndNotify(
|
||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||
? currentPlayer.getCurrentWindowIndex()
|
||||
: C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
private void setCurrentPlayer(Player currentPlayer) {
|
||||
if (this.currentPlayer == currentPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// View management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
localPlayerView.setVisibility(View.VISIBLE);
|
||||
castControlView.hide();
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
localPlayerView.setVisibility(View.GONE);
|
||||
castControlView.show();
|
||||
}
|
||||
|
||||
// Player state management.
|
||||
long playbackPositionMs = C.TIME_UNSET;
|
||||
int windowIndex = C.INDEX_UNSET;
|
||||
boolean playWhenReady = false;
|
||||
|
||||
Player previousPlayer = this.currentPlayer;
|
||||
if (previousPlayer != null) {
|
||||
// Save state from the previous player.
|
||||
int playbackState = previousPlayer.getPlaybackState();
|
||||
if (playbackState != Player.STATE_ENDED) {
|
||||
playbackPositionMs = previousPlayer.getCurrentPosition();
|
||||
playWhenReady = previousPlayer.getPlayWhenReady();
|
||||
windowIndex = previousPlayer.getCurrentWindowIndex();
|
||||
if (windowIndex != currentItemIndex) {
|
||||
playbackPositionMs = C.TIME_UNSET;
|
||||
windowIndex = currentItemIndex;
|
||||
}
|
||||
}
|
||||
previousPlayer.stop(true);
|
||||
}
|
||||
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
|
||||
// Playback transition.
|
||||
if (windowIndex != C.INDEX_UNSET) {
|
||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the item at the given position.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
* @param positionMs The position at which playback should start.
|
||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||
*/
|
||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||
if (this.currentItemIndex != currentItemIndex) {
|
||||
int oldIndex = this.currentItemIndex;
|
||||
this.currentItemIndex = currentItemIndex;
|
||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(MediaItem item) {
|
||||
Uri uri = item.uri;
|
||||
String mimeType = item.mimeType;
|
||||
if (mimeType == null) {
|
||||
throw new IllegalArgumentException("mimeType is required");
|
||||
}
|
||||
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager =
|
||||
DrmSessionManager.getDummyDrmSessionManager();
|
||||
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
|
||||
if (drmConfiguration != null && Util.SDK_INT >= 18) {
|
||||
String licenseServerUrl =
|
||||
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : "";
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY);
|
||||
for (Map.Entry<String, String> requestHeader : drmConfiguration.requestHeaders.entrySet()) {
|
||||
drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue());
|
||||
}
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(/* multiSession= */ true)
|
||||
.setUuidAndExoMediaDrmProvider(
|
||||
drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
}
|
||||
|
||||
MediaSource createdMediaSource;
|
||||
switch (mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
createdMediaSource =
|
||||
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
createdMediaSource =
|
||||
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
createdMediaSource =
|
||||
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
createdMediaSource =
|
||||
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
|
||||
}
|
||||
return createdMediaSource;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,8 @@
|
||||
|
||||
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
||||
|
||||
<string name="player_error_msg">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info.</string>
|
||||
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
|
||||
|
||||
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
|
||||
|
||||
</resources>
|
||||
|
@ -1,4 +0,0 @@
|
||||
# IMA demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with the IMA SDK.
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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.imademo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
||||
/**
|
||||
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
|
||||
* {@link PlayerManager}, which this class instantiates.
|
||||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private PlayerView playerView;
|
||||
private PlayerManager player;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
playerView = findViewById(R.id.player_view);
|
||||
player = new PlayerManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
player.init(this, playerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
player.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
player.release();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
/*
|
||||
* 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.imademo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
|
||||
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
|
||||
|
||||
private final ImaAdsLoader adsLoader;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private long contentPosition;
|
||||
|
||||
public PlayerManager(Context context) {
|
||||
String adTag = context.getString(R.string.ad_tag_url);
|
||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||
dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
}
|
||||
|
||||
public void init(Context context, PlayerView playerView) {
|
||||
// Create a player instance.
|
||||
player = ExoPlayerFactory.newSimpleInstance(context);
|
||||
adsLoader.setPlayer(player);
|
||||
playerView.setPlayer(player);
|
||||
|
||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds =
|
||||
new AdsMediaSource(
|
||||
contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView);
|
||||
|
||||
// Prepare the player with the source.
|
||||
player.seekTo(contentPosition);
|
||||
player.prepare(mediaSourceWithAds);
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
if (player != null) {
|
||||
contentPosition = player.getContentPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
adsLoader.setPlayer(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
adsLoader.release();
|
||||
}
|
||||
|
||||
// AdsMediaSource.MediaSourceFactory implementation.
|
||||
|
||||
@Override
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return buildMediaSource(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
// IMA does not support Smooth Streaming ads.
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri) {
|
||||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<string name="application_name">Exo IMA Demo</string>
|
||||
|
||||
<string name="content_url"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string>
|
||||
|
||||
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
|
||||
|
||||
</resources>
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -26,7 +26,7 @@ android {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -62,15 +62,15 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-core-ui:1.0.0'
|
||||
implementation 'androidx.fragment:fragment:1.0.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
|
||||
|
@ -34,6 +34,7 @@
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:name="com.google.android.exoplayer2.demo.DemoApplication"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
|
@ -376,44 +376,48 @@
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Apple AAC 10s",
|
||||
"name": "Apple 10s (AAC)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
|
||||
},
|
||||
{
|
||||
"name": "Apple TS 10s",
|
||||
"name": "Apple 10s (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
|
||||
},
|
||||
{
|
||||
"name": "Android screens (Matroska)",
|
||||
"name": "Android screens (MKV)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Screens 360P (WebM,VP9,No Audio)",
|
||||
"name": "Screens 360p video (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
|
||||
},
|
||||
{
|
||||
"name": "Screens 480p (FMP4,H264,No Audio)",
|
||||
"name": "Screens 480p video (FMP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Screens 1080p (FMP4,H264, No Audio)",
|
||||
"name": "Screens 1080p video (FMP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Screens (FMP4,AAC Audio)",
|
||||
"name": "Screens audio (FMP4)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (MP3 Audio)",
|
||||
"name": "Google Play (MP3)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (Ogg/Vorbis Audio)",
|
||||
"name": "Google Play (Ogg/Vorbis)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (FLV Video)",
|
||||
"name": "Big Buck Bunny video (FLV)",
|
||||
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny 480p video (MP4,AV1)",
|
||||
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -447,23 +451,27 @@
|
||||
},
|
||||
{
|
||||
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -578,5 +586,17 @@
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Subtitles",
|
||||
"samples": [
|
||||
{
|
||||
"name": "TTML",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
|
||||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_language": "en"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -28,7 +28,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
@ -165,7 +165,7 @@ public class DemoApplication extends Application {
|
||||
return new CacheDataSourceFactory(
|
||||
cache,
|
||||
upstreamFactory,
|
||||
new FileDataSourceFactory(),
|
||||
new FileDataSource.Factory(),
|
||||
/* cacheWriteDataSinkFactory= */ null,
|
||||
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
|
||||
/* eventListener= */ null);
|
||||
|
@ -18,9 +18,9 @@ package com.google.android.exoplayer2.demo;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import android.widget.Toast;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
@ -55,6 +56,7 @@ public class DownloadTracker {
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final HashMap<Uri, Download> downloads;
|
||||
private final DownloadIndex downloadIndex;
|
||||
private final DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
|
||||
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
|
||||
|
||||
@ -65,6 +67,7 @@ public class DownloadTracker {
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
downloads = new HashMap<>();
|
||||
downloadIndex = downloadManager.getDownloadIndex();
|
||||
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
|
||||
downloadManager.addListener(new DownloadManagerListener());
|
||||
loadDownloads();
|
||||
}
|
||||
@ -82,7 +85,6 @@ public class DownloadTracker {
|
||||
return download != null && download.state != Download.STATE_FAILED;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||
@ -124,13 +126,13 @@ public class DownloadTracker {
|
||||
int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
|
||||
return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_SS:
|
||||
return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
|
||||
return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_HLS:
|
||||
return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
|
||||
return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_OTHER:
|
||||
return DownloadHelper.forProgressive(uri);
|
||||
return DownloadHelper.forProgressive(context, uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
@ -203,7 +205,7 @@ public class DownloadTracker {
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
/* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections =*/ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
@ -213,10 +215,13 @@ public class DownloadTracker {
|
||||
|
||||
@Override
|
||||
public void onPrepareError(DownloadHelper helper, IOException e) {
|
||||
Toast.makeText(
|
||||
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Failed to start download", e);
|
||||
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
|
||||
Log.e(
|
||||
TAG,
|
||||
e instanceof DownloadHelper.LiveContentUnsupportedException
|
||||
? "Downloading live content unsupported"
|
||||
: "Failed to start download",
|
||||
e);
|
||||
}
|
||||
|
||||
// DialogInterface.OnClickListener implementation.
|
||||
@ -230,7 +235,7 @@ public class DownloadTracker {
|
||||
downloadHelper.addTrackSelectionForSingleRenderer(
|
||||
periodIndex,
|
||||
/* rendererIndex= */ i,
|
||||
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
||||
trackSelectorParameters,
|
||||
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,9 @@ package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.MediaDrm;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
@ -30,19 +28,24 @@ import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||
@ -50,7 +53,10 @@ import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
@ -66,7 +72,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
||||
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
@ -76,41 +82,51 @@ import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.UUID;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
public class PlayerActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
|
||||
public static final String ACTION_VIEW_LIST =
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
public static final String URI_LIST_EXTRA = "uri_list";
|
||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
||||
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
|
||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||
// Activity extras.
|
||||
|
||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||
|
||||
// Actions.
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
public static final String ACTION_VIEW_LIST =
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
|
||||
// Player configuration extras.
|
||||
|
||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
public static final String URI_EXTRA = "uri";
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
public static final String IS_LIVE_EXTRA = "is_live";
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
public static final String TUNNELING_EXTRA = "tunneling";
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
|
||||
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
|
||||
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
|
||||
// For backwards compatibility only.
|
||||
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
|
||||
// Saved instance state keys.
|
||||
|
||||
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
||||
private static final String KEY_WINDOW = "window";
|
||||
private static final String KEY_POSITION = "position";
|
||||
@ -130,7 +146,6 @@ public class PlayerActivity extends AppCompatActivity
|
||||
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private FrameworkMediaDrm mediaDrm;
|
||||
private MediaSource mediaSource;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
@ -150,7 +165,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
Intent intent = getIntent();
|
||||
String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
setTheme(R.style.PlayerTheme_Spherical);
|
||||
}
|
||||
@ -183,7 +199,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
|
||||
((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
@ -192,7 +208,13 @@ public class PlayerActivity extends AppCompatActivity
|
||||
startWindow = savedInstanceState.getInt(KEY_WINDOW);
|
||||
startPosition = savedInstanceState.getLong(KEY_POSITION);
|
||||
} else {
|
||||
trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
|
||||
DefaultTrackSelector.ParametersBuilder builder =
|
||||
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
|
||||
boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
|
||||
if (Util.SDK_INT >= 21 && tunneling) {
|
||||
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
|
||||
}
|
||||
trackSelectorParameters = builder.build();
|
||||
clearStartPosition();
|
||||
}
|
||||
}
|
||||
@ -326,67 +348,10 @@ public class PlayerActivity extends AppCompatActivity
|
||||
private void initializePlayer() {
|
||||
if (player == null) {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
Uri[] uris;
|
||||
String[] extensions;
|
||||
if (ACTION_VIEW.equals(action)) {
|
||||
uris = new Uri[] {intent.getData()};
|
||||
extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
|
||||
} else if (ACTION_VIEW_LIST.equals(action)) {
|
||||
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
|
||||
uris = new Uri[uriStrings.length];
|
||||
for (int i = 0; i < uriStrings.length; i++) {
|
||||
uris[i] = Uri.parse(uriStrings[i]);
|
||||
}
|
||||
extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
|
||||
if (extensions == null) {
|
||||
extensions = new String[uriStrings.length];
|
||||
}
|
||||
} else {
|
||||
showToast(getString(R.string.unexpected_intent_action, action));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (!Util.checkCleartextTrafficPermitted(uris)) {
|
||||
showToast(R.string.error_cleartext_not_permitted);
|
||||
return;
|
||||
}
|
||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) {
|
||||
// The player will be reinitialized if the permission is granted.
|
||||
return;
|
||||
}
|
||||
|
||||
DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);
|
||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);
|
||||
int errorStringId = R.string.error_drm_unknown;
|
||||
if (Util.SDK_INT < 18) {
|
||||
errorStringId = R.string.error_drm_not_supported;
|
||||
} else {
|
||||
try {
|
||||
String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
|
||||
: DRM_SCHEME_UUID_EXTRA;
|
||||
UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
|
||||
if (drmSchemeUuid == null) {
|
||||
errorStringId = R.string.error_drm_unsupported_scheme;
|
||||
} else {
|
||||
drmSessionManager =
|
||||
buildDrmSessionManagerV18(
|
||||
drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);
|
||||
}
|
||||
} catch (UnsupportedDrmException e) {
|
||||
errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
|
||||
}
|
||||
}
|
||||
if (drmSessionManager == null) {
|
||||
showToast(errorStringId);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
mediaSource = createTopLevelMediaSource(intent);
|
||||
if (mediaSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackSelection.Factory trackSelectionFactory;
|
||||
@ -406,13 +371,14 @@ public class PlayerActivity extends AppCompatActivity
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
|
||||
trackSelector.setParameters(trackSelectorParameters);
|
||||
lastSeenTrackGroupArray = null;
|
||||
|
||||
player =
|
||||
ExoPlayerFactory.newSimpleInstance(
|
||||
/* context= */ this, renderersFactory, trackSelector, drmSessionManager);
|
||||
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build();
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
@ -420,28 +386,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||
playerView.setPlaybackPreparer(this);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
|
||||
MediaSource[] mediaSources = new MediaSource[uris.length];
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
|
||||
}
|
||||
mediaSource =
|
||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
||||
if (adTagUriString != null) {
|
||||
Uri adTagUri = Uri.parse(adTagUriString);
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
|
||||
if (adsMediaSource != null) {
|
||||
mediaSource = adsMediaSource;
|
||||
} else {
|
||||
showToast(R.string.ima_not_loaded);
|
||||
}
|
||||
} else {
|
||||
releaseAdsLoader();
|
||||
if (adsLoader != null) {
|
||||
adsLoader.setPlayer(player);
|
||||
}
|
||||
}
|
||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||
@ -452,34 +398,141 @@ public class PlayerActivity extends AppCompatActivity
|
||||
updateButtonVisibility();
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri) {
|
||||
return buildMediaSource(uri, null);
|
||||
@Nullable
|
||||
private MediaSource createTopLevelMediaSource(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
||||
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
||||
showToast(getString(R.string.unexpected_intent_action, action));
|
||||
finish();
|
||||
return null;
|
||||
}
|
||||
|
||||
Sample intentAsSample = Sample.createFromIntent(intent);
|
||||
UriSample[] samples =
|
||||
intentAsSample instanceof Sample.PlaylistSample
|
||||
? ((Sample.PlaylistSample) intentAsSample).children
|
||||
: new UriSample[] {(UriSample) intentAsSample};
|
||||
|
||||
boolean seenAdsTagUri = false;
|
||||
for (UriSample sample : samples) {
|
||||
seenAdsTagUri |= sample.adTagUri != null;
|
||||
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
|
||||
showToast(R.string.error_cleartext_not_permitted);
|
||||
return null;
|
||||
}
|
||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
|
||||
// The player will be reinitialized if the permission is granted.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MediaSource[] mediaSources = new MediaSource[samples.length];
|
||||
for (int i = 0; i < samples.length; i++) {
|
||||
mediaSources[i] = createLeafMediaSource(samples[i]);
|
||||
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo;
|
||||
if (subtitleInfo != null) {
|
||||
Format subtitleFormat =
|
||||
Format.createTextSampleFormat(
|
||||
/* id= */ null,
|
||||
subtitleInfo.mimeType,
|
||||
C.SELECTION_FLAG_DEFAULT,
|
||||
subtitleInfo.language);
|
||||
MediaSource subtitleMediaSource =
|
||||
new SingleSampleMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
|
||||
mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource);
|
||||
}
|
||||
}
|
||||
MediaSource mediaSource =
|
||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||
|
||||
if (seenAdsTagUri) {
|
||||
Uri adTagUri = samples[0].adTagUri;
|
||||
if (actionIsListView) {
|
||||
showToast(R.string.unsupported_ads_in_concatenation);
|
||||
} else {
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
|
||||
if (adsMediaSource != null) {
|
||||
mediaSource = adsMediaSource;
|
||||
} else {
|
||||
showToast(R.string.ima_not_loaded);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
releaseAdsLoader();
|
||||
}
|
||||
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||
private MediaSource createLeafMediaSource(UriSample parameters) {
|
||||
Sample.DrmInfo drmInfo = parameters.drmInfo;
|
||||
int errorStringId = R.string.error_drm_unknown;
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager = null;
|
||||
if (drmInfo == null) {
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
} else if (Util.SDK_INT < 18) {
|
||||
errorStringId = R.string.error_drm_unsupported_before_api_18;
|
||||
} else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) {
|
||||
errorStringId = R.string.error_drm_unsupported_scheme;
|
||||
} else {
|
||||
MediaDrmCallback mediaDrmCallback =
|
||||
createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.setMultiSession(drmInfo.drmMultiSession)
|
||||
.build(mediaDrmCallback);
|
||||
}
|
||||
|
||||
if (drmSessionManager == null) {
|
||||
showToast(errorStringId);
|
||||
finish();
|
||||
return null;
|
||||
}
|
||||
|
||||
DownloadRequest downloadRequest =
|
||||
((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri);
|
||||
((DemoApplication) getApplication())
|
||||
.getDownloadTracker()
|
||||
.getDownloadRequest(parameters.uri);
|
||||
if (downloadRequest != null) {
|
||||
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
||||
}
|
||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||
return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
|
||||
}
|
||||
|
||||
private MediaSource createLeafMediaSource(
|
||||
Uri uri, String extension, DrmSessionManager<ExoMediaCrypto> drmSessionManager) {
|
||||
@ContentType int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
return new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
return new SsMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
||||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
throws UnsupportedDrmException {
|
||||
private HttpMediaDrmCallback createMediaDrmCallback(
|
||||
String licenseUrl, String[] keyRequestPropertiesArray) {
|
||||
HttpDataSource.Factory licenseDataSourceFactory =
|
||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
@ -490,9 +543,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
releaseMediaDrm();
|
||||
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
||||
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
||||
return drmCallback;
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
@ -509,14 +560,6 @@ public class PlayerActivity extends AppCompatActivity
|
||||
if (adsLoader != null) {
|
||||
adsLoader.setPlayer(null);
|
||||
}
|
||||
releaseMediaDrm();
|
||||
}
|
||||
|
||||
private void releaseMediaDrm() {
|
||||
if (mediaDrm != null) {
|
||||
mediaDrm.release();
|
||||
mediaDrm = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseAdsLoader() {
|
||||
@ -554,7 +597,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||
private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
||||
@Nullable
|
||||
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
||||
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
try {
|
||||
@ -569,12 +613,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
||||
}
|
||||
adsLoader.setPlayer(player);
|
||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||
new AdsMediaSource.MediaSourceFactory() {
|
||||
MediaSourceFactory adMediaSourceFactory =
|
||||
new MediaSourceFactory() {
|
||||
@Override
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return PlayerActivity.this.buildMediaSource(uri);
|
||||
return PlayerActivity.this.createLeafMediaSource(
|
||||
uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -627,7 +671,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
private class PlayerEventListener implements Player.EventListener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
@ -677,7 +721,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) cause;
|
||||
if (decoderInitializationException.decoderName == null) {
|
||||
if (decoderInitializationException.codecInfo == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
@ -692,12 +736,11 @@ public class PlayerActivity extends AppCompatActivity
|
||||
errorString =
|
||||
getString(
|
||||
R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.decoderName);
|
||||
decoderInitializationException.codecInfo.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair.create(0, errorString);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
/* package */ abstract class Sample {
|
||||
|
||||
public static final class UriSample extends Sample {
|
||||
|
||||
public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
|
||||
String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
|
||||
boolean isLive =
|
||||
intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false);
|
||||
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
|
||||
return new UriSample(
|
||||
/* name= */ null,
|
||||
uri,
|
||||
extension,
|
||||
isLive,
|
||||
DrmInfo.createFromIntent(intent, extrasKeySuffix),
|
||||
adTagUri,
|
||||
/* sphericalStereoMode= */ null,
|
||||
SubtitleInfo.createFromIntent(intent, extrasKeySuffix));
|
||||
}
|
||||
|
||||
public final Uri uri;
|
||||
public final String extension;
|
||||
public final boolean isLive;
|
||||
public final DrmInfo drmInfo;
|
||||
public final Uri adTagUri;
|
||||
@Nullable public final String sphericalStereoMode;
|
||||
@Nullable SubtitleInfo subtitleInfo;
|
||||
|
||||
public UriSample(
|
||||
String name,
|
||||
Uri uri,
|
||||
String extension,
|
||||
boolean isLive,
|
||||
DrmInfo drmInfo,
|
||||
Uri adTagUri,
|
||||
@Nullable String sphericalStereoMode,
|
||||
@Nullable SubtitleInfo subtitleInfo) {
|
||||
super(name);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.isLive = isLive;
|
||||
this.drmInfo = drmInfo;
|
||||
this.adTagUri = adTagUri;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
this.subtitleInfo = subtitleInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToIntent(Intent intent) {
|
||||
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
|
||||
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive);
|
||||
intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||
addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
|
||||
}
|
||||
|
||||
public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
|
||||
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive);
|
||||
addPlayerConfigToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
|
||||
private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent
|
||||
.putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
|
||||
.putExtra(
|
||||
AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
|
||||
if (drmInfo != null) {
|
||||
drmInfo.addToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
if (subtitleInfo != null) {
|
||||
subtitleInfo.addToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PlaylistSample extends Sample {
|
||||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(String name, UriSample... children) {
|
||||
super(name);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToIntent(Intent intent) {
|
||||
intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DrmInfo {
|
||||
|
||||
public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
|
||||
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
|
||||
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
|
||||
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
|
||||
return null;
|
||||
}
|
||||
String drmSchemeExtra =
|
||||
intent.hasExtra(schemeKey)
|
||||
? intent.getStringExtra(schemeKey)
|
||||
: intent.getStringExtra(schemeUuidKey);
|
||||
UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
|
||||
boolean drmMultiSession =
|
||||
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
|
||||
return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession);
|
||||
}
|
||||
|
||||
public final UUID drmScheme;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public DrmInfo(
|
||||
UUID drmScheme,
|
||||
String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties,
|
||||
boolean drmMultiSession) {
|
||||
this.drmScheme = drmScheme;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
}
|
||||
|
||||
public void addToIntent(Intent intent, String extrasKeySuffix) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
|
||||
intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
|
||||
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
|
||||
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class SubtitleInfo {
|
||||
|
||||
@Nullable
|
||||
public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) {
|
||||
if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
|
||||
return null;
|
||||
}
|
||||
return new SubtitleInfo(
|
||||
Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
|
||||
intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix),
|
||||
intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix));
|
||||
}
|
||||
|
||||
public final Uri uri;
|
||||
public final String mimeType;
|
||||
@Nullable public final String language;
|
||||
|
||||
public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) {
|
||||
this.uri = Assertions.checkNotNull(uri);
|
||||
this.mimeType = Assertions.checkNotNull(mimeType);
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public void addToIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString());
|
||||
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType);
|
||||
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language);
|
||||
}
|
||||
}
|
||||
|
||||
public static Sample createFromIntent(Intent intent) {
|
||||
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
|
||||
ArrayList<String> intentUris = new ArrayList<>();
|
||||
int index = 0;
|
||||
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
|
||||
intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
|
||||
index++;
|
||||
}
|
||||
UriSample[] children = new UriSample[intentUris.size()];
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
Uri uri = Uri.parse(intentUris.get(i));
|
||||
children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
return new PlaylistSample(/* name= */ null, children);
|
||||
} else {
|
||||
return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable public final String name;
|
||||
|
||||
public Sample(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public abstract void addToIntent(Intent intent);
|
||||
}
|
@ -21,8 +21,6 @@ import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.JsonReader;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -36,8 +34,13 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.demo.Sample.DrmInfo;
|
||||
import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
|
||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||
@ -65,6 +68,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
private SampleAdapter sampleAdapter;
|
||||
private MenuItem preferExtensionDecodersMenuItem;
|
||||
private MenuItem randomAbrMenuItem;
|
||||
private MenuItem tunnelingMenuItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
@ -122,6 +126,10 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
|
||||
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
|
||||
randomAbrMenuItem = menu.findItem(R.id.random_abr);
|
||||
tunnelingMenuItem = menu.findItem(R.id.tunneling);
|
||||
if (Util.SDK_INT < 21) {
|
||||
tunnelingMenuItem.setEnabled(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -161,13 +169,18 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
public boolean onChildClick(
|
||||
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
||||
Sample sample = (Sample) view.getTag();
|
||||
startActivity(
|
||||
sample.buildIntent(
|
||||
/* context= */ this,
|
||||
isNonNullAndChecked(preferExtensionDecodersMenuItem),
|
||||
isNonNullAndChecked(randomAbrMenuItem)
|
||||
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
||||
: PlayerActivity.ABR_ALGORITHM_DEFAULT));
|
||||
Intent intent = new Intent(this, PlayerActivity.class);
|
||||
intent.putExtra(
|
||||
PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
String abrAlgorithm =
|
||||
isNonNullAndChecked(randomAbrMenuItem)
|
||||
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
||||
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
|
||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||
intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
|
||||
sample.addToIntent(intent);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -198,6 +211,9 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
if (uriSample.drmInfo != null) {
|
||||
return R.string.download_drm_unsupported;
|
||||
}
|
||||
if (uriSample.isLive) {
|
||||
return R.string.download_live_unsupported;
|
||||
}
|
||||
if (uriSample.adTagUri != null) {
|
||||
return R.string.download_ads_unsupported;
|
||||
}
|
||||
@ -287,6 +303,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
String sampleName = null;
|
||||
Uri uri = null;
|
||||
String extension = null;
|
||||
boolean isLive = false;
|
||||
String drmScheme = null;
|
||||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
@ -294,6 +311,10 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
String sphericalStereoMode = null;
|
||||
List<Sample.SubtitleInfo> subtitleInfos = new ArrayList<>();
|
||||
Uri subtitleUri = null;
|
||||
String subtitleMimeType = null;
|
||||
String subtitleLanguage = null;
|
||||
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
@ -309,17 +330,15 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
extension = reader.nextString();
|
||||
break;
|
||||
case "drm_scheme":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
||||
drmScheme = reader.nextString();
|
||||
break;
|
||||
case "is_live":
|
||||
isLive = reader.nextBoolean();
|
||||
break;
|
||||
case "drm_license_url":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
"Invalid attribute on nested item: drm_license_url");
|
||||
drmLicenseUrl = reader.nextString();
|
||||
break;
|
||||
case "drm_key_request_properties":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
"Invalid attribute on nested item: drm_key_request_properties");
|
||||
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
@ -337,7 +356,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
playlistSamples = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
playlistSamples.add((UriSample) readEntry(reader, true));
|
||||
playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
@ -349,6 +368,15 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
|
||||
sphericalStereoMode = reader.nextString();
|
||||
break;
|
||||
case "subtitle_uri":
|
||||
subtitleUri = Uri.parse(reader.nextString());
|
||||
break;
|
||||
case "subtitle_mime_type":
|
||||
subtitleMimeType = reader.nextString();
|
||||
break;
|
||||
case "subtitle_language":
|
||||
subtitleLanguage = reader.nextString();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
}
|
||||
@ -357,18 +385,32 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
DrmInfo drmInfo =
|
||||
drmScheme == null
|
||||
? null
|
||||
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
|
||||
: new DrmInfo(
|
||||
Util.getDrmUuid(drmScheme),
|
||||
drmLicenseUrl,
|
||||
drmKeyRequestProperties,
|
||||
drmMultiSession);
|
||||
Sample.SubtitleInfo subtitleInfo =
|
||||
subtitleUri == null
|
||||
? null
|
||||
: new Sample.SubtitleInfo(
|
||||
subtitleUri,
|
||||
Assertions.checkNotNull(
|
||||
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
|
||||
subtitleLanguage);
|
||||
if (playlistSamples != null) {
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
||||
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
|
||||
return new PlaylistSample(sampleName, playlistSamplesArray);
|
||||
} else {
|
||||
return new UriSample(
|
||||
sampleName,
|
||||
drmInfo,
|
||||
uri,
|
||||
extension,
|
||||
adTagUri,
|
||||
sphericalStereoMode);
|
||||
isLive,
|
||||
drmInfo,
|
||||
adTagUri != null ? Uri.parse(adTagUri) : null,
|
||||
sphericalStereoMode,
|
||||
subtitleInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -480,7 +522,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
ImageButton downloadButton = view.findViewById(R.id.download_button);
|
||||
downloadButton.setTag(sample);
|
||||
downloadButton.setColorFilter(
|
||||
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE);
|
||||
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
|
||||
downloadButton.setImageResource(
|
||||
isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
|
||||
}
|
||||
@ -497,116 +539,4 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class DrmInfo {
|
||||
public final String drmScheme;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public DrmInfo(
|
||||
String drmScheme,
|
||||
String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties,
|
||||
boolean drmMultiSession) {
|
||||
this.drmScheme = drmScheme;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
}
|
||||
|
||||
public void updateIntent(Intent intent) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);
|
||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class Sample {
|
||||
public final String name;
|
||||
public final DrmInfo drmInfo;
|
||||
|
||||
public Sample(String name, DrmInfo drmInfo) {
|
||||
this.name = name;
|
||||
this.drmInfo = drmInfo;
|
||||
}
|
||||
|
||||
public Intent buildIntent(
|
||||
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||
Intent intent = new Intent(context, PlayerActivity.class);
|
||||
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
|
||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||
if (drmInfo != null) {
|
||||
drmInfo.updateIntent(intent);
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class UriSample extends Sample {
|
||||
|
||||
public final Uri uri;
|
||||
public final String extension;
|
||||
public final String adTagUri;
|
||||
public final String sphericalStereoMode;
|
||||
|
||||
public UriSample(
|
||||
String name,
|
||||
DrmInfo drmInfo,
|
||||
Uri uri,
|
||||
String extension,
|
||||
String adTagUri,
|
||||
String sphericalStereoMode) {
|
||||
super(name, drmInfo);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.adTagUri = adTagUri;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent buildIntent(
|
||||
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
|
||||
.setData(uri)
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)
|
||||
.setAction(PlayerActivity.ACTION_VIEW);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class PlaylistSample extends Sample {
|
||||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(
|
||||
String name,
|
||||
DrmInfo drmInfo,
|
||||
UriSample... children) {
|
||||
super(name, drmInfo);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent buildIntent(
|
||||
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||
String[] uris = new String[children.length];
|
||||
String[] extensions = new String[children.length];
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
uris[i] = children[i].uri.toString();
|
||||
extensions[i] = children[i].extension;
|
||||
}
|
||||
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
|
||||
.putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
|
||||
.putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
|
||||
.setAction(PlayerActivity.ACTION_VIEW_LIST);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,19 +19,18 @@ import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.appcompat.app.AppCompatDialog;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -23,4 +23,8 @@
|
||||
android:title="@string/random_abr"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/tunneling"
|
||||
android:title="@string/tunneling"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
<string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
@ -53,6 +53,8 @@
|
||||
|
||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||
|
||||
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
|
||||
|
||||
<string name="download_start_error">Failed to start download</string>
|
||||
|
||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||
@ -61,10 +63,14 @@
|
||||
|
||||
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
|
||||
|
||||
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
|
||||
|
||||
<string name="download_ads_unsupported">IMA does not support offline ads</string>
|
||||
|
||||
<string name="prefer_extension_decoders">Prefer extension decoders</string>
|
||||
|
||||
<string name="random_abr">Enable random ABR</string>
|
||||
|
||||
<string name="tunneling">Request multimedia tunneling</string>
|
||||
|
||||
</resources>
|
||||
|
@ -24,7 +24,7 @@
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme.Spherical">
|
||||
<item name="surface_type">spherical_view</item>
|
||||
<item name="surface_type">spherical_gl_surface_view</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
21
demos/surface/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# ExoPlayer SurfaceControl demo
|
||||
|
||||
This app demonstrates how to use the [SurfaceControl][] API to redirect video
|
||||
output from ExoPlayer between different views or off-screen. `SurfaceControl`
|
||||
is new in Android 10, so the app requires `minSdkVersion` 29.
|
||||
|
||||
The app layout has a grid of `SurfaceViews`. Initially video is output to one
|
||||
of the views. Tap a `SurfaceView` to move video output to it. You can also tap
|
||||
the buttons at the top of the activity to move video output off-screen, to a
|
||||
full-screen `SurfaceView` or to a new activity.
|
||||
|
||||
When using `SurfaceControl`, the `MediaCodec` always has the same surface
|
||||
attached to it, which can be freely 'reparented' to any `SurfaceView` (or
|
||||
off-screen) without any interruptions to playback. This works better than
|
||||
calling `MediaCodec.setOutputSurface` to change the output surface of the codec
|
||||
because `MediaCodec` does not re-render its last frame when that method is
|
||||
called, and because you can move output off-screen easily (`setOutputSurface`
|
||||
can't take a `null` surface, so the player has to use a `DummySurface`, which
|
||||
doesn't handle protected output on all devices).
|
||||
|
||||
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
|
@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
// Copyright (C) 2019 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.
|
||||
@ -25,8 +25,8 @@ android {
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
minSdkVersion 29
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -35,14 +35,11 @@ android {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
// This demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,10 +47,5 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
<!-- Copyright (C) 2019 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.
|
||||
@ -14,25 +14,28 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
package="com.google.android.exoplayer2.surfacedemo">
|
||||
<uses-sdk/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name">
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.surfacedemo.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="asset"/>
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.surfacedemo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Activity that demonstrates use of {@link SurfaceControl} with ExoPlayer. */
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private static final String DEFAULT_MEDIA_URI =
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";
|
||||
private static final String SURFACE_CONTROL_NAME = "surfacedemo";
|
||||
|
||||
private static final String ACTION_VIEW = "com.google.android.exoplayer.surfacedemo.action.VIEW";
|
||||
private static final String EXTENSION_EXTRA = "extension";
|
||||
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
private static final String OWNER_EXTRA = "owner";
|
||||
|
||||
private boolean isOwner;
|
||||
@Nullable private PlayerControlView playerControlView;
|
||||
@Nullable private SurfaceView fullScreenView;
|
||||
@Nullable private SurfaceView nonFullScreenView;
|
||||
@Nullable private SurfaceView currentOutputView;
|
||||
|
||||
@Nullable private static SimpleExoPlayer player;
|
||||
@Nullable private static SurfaceControl surfaceControl;
|
||||
@Nullable private static Surface videoSurface;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
playerControlView = findViewById(R.id.player_control_view);
|
||||
fullScreenView = findViewById(R.id.full_screen_view);
|
||||
fullScreenView.setOnClickListener(
|
||||
v -> {
|
||||
setCurrentOutputView(nonFullScreenView);
|
||||
Assertions.checkNotNull(fullScreenView).setVisibility(View.GONE);
|
||||
});
|
||||
attachSurfaceListener(fullScreenView);
|
||||
isOwner = getIntent().getBooleanExtra(OWNER_EXTRA, /* defaultValue= */ true);
|
||||
GridLayout gridLayout = findViewById(R.id.grid_layout);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
View view;
|
||||
if (i == 0) {
|
||||
Button button = new Button(/* context= */ this);
|
||||
view = button;
|
||||
button.setText(getString(R.string.no_output_label));
|
||||
button.setOnClickListener(v -> reparent(/* surfaceView= */ null));
|
||||
} else if (i == 1) {
|
||||
Button button = new Button(/* context= */ this);
|
||||
view = button;
|
||||
button.setText(getString(R.string.full_screen_label));
|
||||
button.setOnClickListener(
|
||||
v -> {
|
||||
setCurrentOutputView(fullScreenView);
|
||||
Assertions.checkNotNull(fullScreenView).setVisibility(View.VISIBLE);
|
||||
});
|
||||
} else if (i == 2) {
|
||||
Button button = new Button(/* context= */ this);
|
||||
view = button;
|
||||
button.setText(getString(R.string.new_activity_label));
|
||||
button.setOnClickListener(
|
||||
v ->
|
||||
startActivity(
|
||||
new Intent(MainActivity.this, MainActivity.class)
|
||||
.putExtra(OWNER_EXTRA, /* value= */ false)));
|
||||
} else {
|
||||
SurfaceView surfaceView = new SurfaceView(this);
|
||||
view = surfaceView;
|
||||
attachSurfaceListener(surfaceView);
|
||||
surfaceView.setOnClickListener(
|
||||
v -> {
|
||||
setCurrentOutputView(surfaceView);
|
||||
nonFullScreenView = surfaceView;
|
||||
});
|
||||
if (nonFullScreenView == null) {
|
||||
nonFullScreenView = surfaceView;
|
||||
}
|
||||
}
|
||||
gridLayout.addView(view);
|
||||
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
|
||||
layoutParams.width = 0;
|
||||
layoutParams.height = 0;
|
||||
layoutParams.columnSpec = GridLayout.spec(i % 3, 1f);
|
||||
layoutParams.rowSpec = GridLayout.spec(i / 3, 1f);
|
||||
layoutParams.bottomMargin = 10;
|
||||
layoutParams.leftMargin = 10;
|
||||
layoutParams.topMargin = 10;
|
||||
layoutParams.rightMargin = 10;
|
||||
view.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (isOwner && player == null) {
|
||||
initializePlayer();
|
||||
}
|
||||
|
||||
setCurrentOutputView(nonFullScreenView);
|
||||
|
||||
PlayerControlView playerControlView = Assertions.checkNotNull(this.playerControlView);
|
||||
playerControlView.setPlayer(player);
|
||||
playerControlView.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
Assertions.checkNotNull(playerControlView).setPlayer(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (isOwner && isFinishing()) {
|
||||
if (surfaceControl != null) {
|
||||
surfaceControl.release();
|
||||
surfaceControl = null;
|
||||
}
|
||||
if (videoSurface != null) {
|
||||
videoSurface.release();
|
||||
videoSurface = null;
|
||||
}
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePlayer() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
Uri uri =
|
||||
ACTION_VIEW.equals(action)
|
||||
? Assertions.checkNotNull(intent.getData())
|
||||
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
||||
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
||||
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
||||
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
} else {
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
this, Util.getUserAgent(this, getString(R.string.application_name)));
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||
|
||||
surfaceControl =
|
||||
new SurfaceControl.Builder()
|
||||
.setName(SURFACE_CONTROL_NAME)
|
||||
.setBufferSize(/* width= */ 0, /* height= */ 0)
|
||||
.build();
|
||||
videoSurface = new Surface(surfaceControl);
|
||||
player.setVideoSurface(videoSurface);
|
||||
MainActivity.player = player;
|
||||
}
|
||||
|
||||
private void setCurrentOutputView(@Nullable SurfaceView surfaceView) {
|
||||
currentOutputView = surfaceView;
|
||||
if (surfaceView != null && surfaceView.getHolder().getSurface() != null) {
|
||||
reparent(surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
private void attachSurfaceListener(SurfaceView surfaceView) {
|
||||
surfaceView
|
||||
.getHolder()
|
||||
.addCallback(
|
||||
new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder surfaceHolder) {
|
||||
if (surfaceView == currentOutputView) {
|
||||
reparent(surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(
|
||||
SurfaceHolder surfaceHolder, int format, int width, int height) {}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {}
|
||||
});
|
||||
}
|
||||
|
||||
private static void reparent(@Nullable SurfaceView surfaceView) {
|
||||
SurfaceControl surfaceControl = Assertions.checkNotNull(MainActivity.surfaceControl);
|
||||
if (surfaceView == null) {
|
||||
new SurfaceControl.Transaction()
|
||||
.reparent(surfaceControl, /* newParent= */ null)
|
||||
.setBufferSize(surfaceControl, /* w= */ 0, /* h= */ 0)
|
||||
.setVisibility(surfaceControl, /* visible= */ false)
|
||||
.apply();
|
||||
} else {
|
||||
SurfaceControl newParentSurfaceControl = surfaceView.getSurfaceControl();
|
||||
new SurfaceControl.Transaction()
|
||||
.reparent(surfaceControl, newParentSurfaceControl)
|
||||
.setBufferSize(surfaceControl, surfaceView.getWidth(), surfaceView.getHeight())
|
||||
.setVisibility(surfaceControl, /* visible= */ true)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
}
|
51
demos/surface/src/main/res/layout/main_activity.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 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.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/grid_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:columnCount="3"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView
|
||||
android:id="@+id/player_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
app:show_timeout="0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/full_screen_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
<!-- Copyright (C) 2019 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.
|
||||
@ -14,5 +14,10 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<style name="VrTheme" parent="android:Theme.Material"/>
|
||||
|
||||
<string name="application_name">ExoPlayer SurfaceControl demo</string>
|
||||
<string name="no_output_label">No output</string>
|
||||
<string name="full_screen_label">Full screen</string>
|
||||
<string name="new_activity_label">New activity</string>
|
||||
|
||||
</resources>
|
126
extensions/av1/README.md
Normal file
@ -0,0 +1,126 @@
|
||||
# ExoPlayer AV1 extension #
|
||||
|
||||
The AV1 extension provides `Libgav1VideoRenderer`, which uses libgav1 native
|
||||
library to decode AV1 videos.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions (Linux, macOS) ##
|
||||
|
||||
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 fetch cpu_features library and libgav1 with its
|
||||
dependencies as follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
```
|
||||
cd "<path to exoplayer checkout>"
|
||||
EXOPLAYER_ROOT="$(pwd)"
|
||||
AV1_EXT_PATH="${EXOPLAYER_ROOT}/extensions/av1/src/main"
|
||||
```
|
||||
|
||||
* Fetch cpu_features library:
|
||||
|
||||
```
|
||||
cd "${AV1_EXT_PATH}/jni" && \
|
||||
git clone https://github.com/google/cpu_features
|
||||
```
|
||||
|
||||
* Fetch libgav1:
|
||||
|
||||
```
|
||||
cd "${AV1_EXT_PATH}/jni" && \
|
||||
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
|
||||
```
|
||||
|
||||
* Fetch Abseil:
|
||||
|
||||
```
|
||||
cd "${AV1_EXT_PATH}/jni/libgav1" && \
|
||||
git clone https://github.com/abseil/abseil-cpp.git third_party/abseil-cpp
|
||||
```
|
||||
|
||||
* [Install CMake][].
|
||||
|
||||
Having followed these steps, gradle will build the extension automatically when
|
||||
run on the command line or via Android Studio, using [CMake][] and [Ninja][]
|
||||
to configure and build libgav1 and the extension's [JNI wrapper library][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Install CMake]: https://developer.android.com/studio/projects/install-ndk
|
||||
[CMake]: https://cmake.org/
|
||||
[Ninja]: https://ninja-build.org
|
||||
[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc
|
||||
|
||||
## Build instructions (Windows) ##
|
||||
|
||||
We do not provide support for building this extension on Windows, however it
|
||||
should be possible to follow the Linux instructions in [Windows PowerShell][].
|
||||
|
||||
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
|
||||
|
||||
## 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 `Libgav1VideoRenderer`.
|
||||
How you do this depends on which player API you're using:
|
||||
|
||||
* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
|
||||
you can enable using the extension by setting the `extensionRendererMode`
|
||||
parameter of the `DefaultRenderersFactory` constructor to
|
||||
`EXTENSION_RENDERER_MODE_ON`. This will use `Libgav1VideoRenderer` for
|
||||
playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1
|
||||
stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `Libgav1VideoRenderer`
|
||||
priority over `MediaCodecVideoRenderer`.
|
||||
* If you've subclassed `DefaultRenderersFactory`, add a `Libvgav1VideoRenderer`
|
||||
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
|
||||
`Libgav1VideoRenderer` instance from `createRenderers`. ExoPlayer will use the
|
||||
first `Renderer` in the returned array that supports the input media format.
|
||||
* If you're using `ExoPlayer.Builder`, pass a `Libgav1VideoRenderer` 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.
|
||||
You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
|
||||
then you need to implement your own logic to use the renderer for a given track.
|
||||
|
||||
## Rendering options ##
|
||||
|
||||
There are two possibilities for rendering the output `Libgav1VideoRenderer`
|
||||
gets from the libgav1 decoder:
|
||||
|
||||
* GL rendering using GL shader for color space conversion
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
|
||||
setting `surface_type` of `PlayerView` to be
|
||||
`video_decoder_gl_surface_view`.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
|
||||
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
|
||||
`VideoDecoderOutputBufferRenderer` as its object.
|
||||
|
||||
* Native rendering using `ANativeWindow`
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
|
||||
by default.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
|
||||
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
|
||||
|
||||
Note: Although the default option uses `ANativeWindow`, based on our testing the
|
||||
GL rendering mode has better performance, so should be preferred
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.av1.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
73
extensions/av1/build.gradle
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2019 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'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
// Debug CMake build type causes video frames to drop,
|
||||
// so native library should always use Release build type.
|
||||
arguments "-DCMAKE_BUILD_TYPE=Release"
|
||||
targets "gav1JNI"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This option resolves the problem of finding libgav1JNI.so
|
||||
// on multiple paths. The first one found is picked.
|
||||
packagingOptions {
|
||||
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
|
||||
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
|
||||
pickFirst 'lib/x86/libgav1JNI.so'
|
||||
pickFirst 'lib/x86_64/libgav1JNI.so'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
// As native JNI library build is invoked from gradle, this is
|
||||
// not needed. However, it exposes the built library and keeps
|
||||
// consistency with the other extensions.
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the native build only if libgav1 is present, to avoid gradle sync
|
||||
// failures if libgav1 hasn't been checked out according to the README and CMake
|
||||
// isn't installed.
|
||||
if (project.file('src/main/jni/libgav1').exists()) {
|
||||
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
|
||||
android.externalNativeBuild.cmake.version = '3.7.1+'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'AV1 extension'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
7
extensions/av1/proguard-rules.txt
Normal file
@ -0,0 +1,7 @@
|
||||
# Proguard rules specific to the AV1 extension.
|
||||
|
||||
# This prevents the names of native methods from being obfuscated.
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
<!-- Copyright (C) 2019 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.
|
||||
@ -14,4 +14,4 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.testutil"/>
|
||||
<manifest package="com.google.android.exoplayer2.ext.av1"/>
|
@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** Gav1 decoder. */
|
||||
/* package */ final class Gav1Decoder
|
||||
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> {
|
||||
|
||||
// LINT.IfChange
|
||||
private static final int GAV1_ERROR = 0;
|
||||
private static final int GAV1_OK = 1;
|
||||
private static final int GAV1_DECODE_ONLY = 2;
|
||||
// LINT.ThenChange(../../../../../../../jni/gav1_jni.cc)
|
||||
|
||||
private final long gav1DecoderContext;
|
||||
|
||||
@C.VideoOutputMode private volatile int outputMode;
|
||||
|
||||
/**
|
||||
* Creates a Gav1Decoder.
|
||||
*
|
||||
* @param numInputBuffers Number of input buffers.
|
||||
* @param numOutputBuffers Number of output buffers.
|
||||
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
|
||||
* @param threads Number of threads libgav1 will use to decode.
|
||||
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
|
||||
*/
|
||||
public Gav1Decoder(
|
||||
int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, int threads)
|
||||
throws Gav1DecoderException {
|
||||
super(
|
||||
new VideoDecoderInputBuffer[numInputBuffers],
|
||||
new VideoDecoderOutputBuffer[numOutputBuffers]);
|
||||
if (!Gav1Library.isAvailable()) {
|
||||
throw new Gav1DecoderException("Failed to load decoder native library.");
|
||||
}
|
||||
gav1DecoderContext = gav1Init(threads);
|
||||
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
|
||||
throw new Gav1DecoderException(
|
||||
"Failed to initialize decoder. Error: " + gav1GetErrorMessage(gav1DecoderContext));
|
||||
}
|
||||
setInitialInputBufferSize(initialInputBufferSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "libgav1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output mode for frames rendered by the decoder.
|
||||
*
|
||||
* @param outputMode The output mode.
|
||||
*/
|
||||
public void setOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
this.outputMode = outputMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VideoDecoderInputBuffer createInputBuffer() {
|
||||
return new VideoDecoderInputBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VideoDecoderOutputBuffer createOutputBuffer() {
|
||||
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Gav1DecoderException decode(
|
||||
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
|
||||
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
|
||||
int inputSize = inputData.limit();
|
||||
if (gav1Decode(gav1DecoderContext, inputData, inputSize) == GAV1_ERROR) {
|
||||
return new Gav1DecoderException(
|
||||
"gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
|
||||
}
|
||||
|
||||
boolean decodeOnly = inputBuffer.isDecodeOnly();
|
||||
if (!decodeOnly) {
|
||||
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
|
||||
}
|
||||
// We need to dequeue the decoded frame from the decoder even when the input data is
|
||||
// decode-only.
|
||||
int getFrameResult = gav1GetFrame(gav1DecoderContext, outputBuffer, decodeOnly);
|
||||
if (getFrameResult == GAV1_ERROR) {
|
||||
return new Gav1DecoderException(
|
||||
"gav1GetFrame error: " + gav1GetErrorMessage(gav1DecoderContext));
|
||||
}
|
||||
if (getFrameResult == GAV1_DECODE_ONLY) {
|
||||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
}
|
||||
if (!decodeOnly) {
|
||||
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Gav1DecoderException createUnexpectedDecodeException(Throwable error) {
|
||||
return new Gav1DecoderException("Unexpected decode error", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
super.release();
|
||||
gav1Close(gav1DecoderContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
|
||||
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
|
||||
// require a call to gav1ReleaseFrame.
|
||||
if (buffer.mode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
|
||||
gav1ReleaseFrame(gav1DecoderContext, buffer);
|
||||
}
|
||||
super.releaseOutputBuffer(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders output buffer to the given surface. Must only be called when in {@link
|
||||
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
|
||||
*
|
||||
* @param outputBuffer Output buffer.
|
||||
* @param surface Output surface.
|
||||
* @throws Gav1DecoderException Thrown if called with invalid output mode or frame rendering
|
||||
* fails.
|
||||
*/
|
||||
public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
|
||||
throws Gav1DecoderException {
|
||||
if (outputBuffer.mode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) {
|
||||
throw new Gav1DecoderException("Invalid output mode.");
|
||||
}
|
||||
if (gav1RenderFrame(gav1DecoderContext, surface, outputBuffer) == GAV1_ERROR) {
|
||||
throw new Gav1DecoderException(
|
||||
"Buffer render error: " + gav1GetErrorMessage(gav1DecoderContext));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a libgav1 decoder.
|
||||
*
|
||||
* @param threads Number of threads to be used by a libgav1 decoder.
|
||||
* @return The address of the decoder context or {@link #GAV1_ERROR} if there was an error.
|
||||
*/
|
||||
private native long gav1Init(int threads);
|
||||
|
||||
/**
|
||||
* Deallocates the decoder context.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
*/
|
||||
private native void gav1Close(long context);
|
||||
|
||||
/**
|
||||
* Decodes the encoded data passed.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @param encodedData Encoded data.
|
||||
* @param length Length of the data buffer.
|
||||
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
|
||||
*/
|
||||
private native int gav1Decode(long context, ByteBuffer encodedData, int length);
|
||||
|
||||
/**
|
||||
* Gets the decoded frame.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @param outputBuffer Output buffer for the decoded frame.
|
||||
* @return {@link #GAV1_OK} if successful, {@link #GAV1_DECODE_ONLY} if successful but the frame
|
||||
* is decode-only, {@link #GAV1_ERROR} if an error occurred.
|
||||
*/
|
||||
private native int gav1GetFrame(
|
||||
long context, VideoDecoderOutputBuffer outputBuffer, boolean decodeOnly);
|
||||
|
||||
/**
|
||||
* Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @param surface Output surface.
|
||||
* @param outputBuffer Output buffer with the decoded frame.
|
||||
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured.
|
||||
*/
|
||||
private native int gav1RenderFrame(
|
||||
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
|
||||
|
||||
/**
|
||||
* Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @param outputBuffer Output buffer.
|
||||
*/
|
||||
private native void gav1ReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
|
||||
|
||||
/**
|
||||
* Returns a human-readable string describing the last error encountered in the given context.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @return A string describing the last encountered error.
|
||||
*/
|
||||
private native String gav1GetErrorMessage(long context);
|
||||
|
||||
/**
|
||||
* Returns whether an error occured.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
|
||||
*/
|
||||
private native int gav1CheckError(long context);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import com.google.android.exoplayer2.video.VideoDecoderException;
|
||||
|
||||
/** Thrown when a libgav1 decoder error occurs. */
|
||||
public final class Gav1DecoderException extends VideoDecoderException {
|
||||
|
||||
/* package */ Gav1DecoderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/* package */ Gav1DecoderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||
|
||||
/** Configures and queries the underlying native library. */
|
||||
public final class Gav1Library {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.gav1");
|
||||
}
|
||||
|
||||
private static final LibraryLoader LOADER = new LibraryLoader("gav1JNI");
|
||||
|
||||
private Gav1Library() {}
|
||||
|
||||
/** Returns whether the underlying library is available, loading it if necessary. */
|
||||
public static boolean isAvailable() {
|
||||
return LOADER.isAvailable();
|
||||
}
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import static java.lang.Runtime.getRuntime;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderException;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
|
||||
/**
|
||||
* Decodes and renders video using libgav1 decoder.
|
||||
*
|
||||
* <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
|
||||
* on the playback thread:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
|
||||
* should be the target {@link Surface}, or null.
|
||||
* <li>Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
|
||||
* buffer renderer. The message payload should be the target {@link
|
||||
* VideoDecoderOutputBufferRenderer}, or null.
|
||||
* </ul>
|
||||
*/
|
||||
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
|
||||
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
|
||||
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
|
||||
/* Default size based on 720p resolution video compressed by a factor of two. */
|
||||
private static final int DEFAULT_INPUT_BUFFER_SIZE =
|
||||
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
|
||||
|
||||
/** The number of input buffers. */
|
||||
private final int numInputBuffers;
|
||||
/**
|
||||
* The number of output buffers. The renderer may limit the minimum possible value due to
|
||||
* requiring multiple output buffers to be dequeued at a time for it to make progress.
|
||||
*/
|
||||
private final int numOutputBuffers;
|
||||
|
||||
private final int threads;
|
||||
|
||||
@Nullable private Gav1Decoder decoder;
|
||||
|
||||
/**
|
||||
* Creates a Libgav1VideoRenderer.
|
||||
*
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||
*/
|
||||
public Libgav1VideoRenderer(
|
||||
long allowedJoiningTimeMs,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify) {
|
||||
this(
|
||||
allowedJoiningTimeMs,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
maxDroppedFramesToNotify,
|
||||
/* threads= */ getRuntime().availableProcessors(),
|
||||
DEFAULT_NUM_OF_INPUT_BUFFERS,
|
||||
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Libgav1VideoRenderer.
|
||||
*
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||
* @param threads Number of threads libgav1 will use to decode.
|
||||
* @param numInputBuffers Number of input buffers.
|
||||
* @param numOutputBuffers Number of output buffers.
|
||||
*/
|
||||
public Libgav1VideoRenderer(
|
||||
long allowedJoiningTimeMs,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify,
|
||||
int threads,
|
||||
int numInputBuffers,
|
||||
int numOutputBuffers) {
|
||||
super(
|
||||
allowedJoiningTimeMs,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
maxDroppedFramesToNotify,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false);
|
||||
this.threads = threads;
|
||||
this.numInputBuffers = numInputBuffers;
|
||||
this.numOutputBuffers = numOutputBuffers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Capabilities
|
||||
protected int supportsFormatInternal(
|
||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
||||
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|
||||
|| !Gav1Library.isAvailable()) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
|
||||
}
|
||||
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||
}
|
||||
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SimpleDecoder<
|
||||
VideoDecoderInputBuffer,
|
||||
? extends VideoDecoderOutputBuffer,
|
||||
? extends VideoDecoderException>
|
||||
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws VideoDecoderException {
|
||||
TraceUtil.beginSection("createGav1Decoder");
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
Gav1Decoder decoder =
|
||||
new Gav1Decoder(numInputBuffers, numOutputBuffers, initialInputBufferSize, threads);
|
||||
this.decoder = decoder;
|
||||
TraceUtil.endSection();
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
|
||||
throws Gav1DecoderException {
|
||||
if (decoder == null) {
|
||||
throw new Gav1DecoderException(
|
||||
"Failed to render output buffer to surface: decoder is not initialized.");
|
||||
}
|
||||
decoder.renderToSurface(outputBuffer, surface);
|
||||
outputBuffer.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
if (decoder != null) {
|
||||
decoder.setOutputMode(outputMode);
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerMessage.Target implementation.
|
||||
|
||||
@Override
|
||||
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
|
||||
if (messageType == C.MSG_SET_SURFACE) {
|
||||
setOutputSurface((Surface) message);
|
||||
} else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
|
||||
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
|
||||
} else {
|
||||
super.handleMessage(messageType, message);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
56
extensions/av1/src/main/jni/CMakeLists.txt
Normal file
@ -0,0 +1,56 @@
|
||||
# libgav1JNI requires modern CMake.
|
||||
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
|
||||
|
||||
# libgav1JNI requires C++11.
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
project(libgav1JNI C CXX)
|
||||
|
||||
# Devices using armeabi-v7a are not required to support
|
||||
# Neon which is why Neon is disabled by default for
|
||||
# armeabi-v7a build. This flag enables it.
|
||||
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
|
||||
add_compile_options("-mfpu=neon")
|
||||
add_compile_options("-fPIC")
|
||||
endif()
|
||||
|
||||
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
|
||||
set(libgav1_jni_output_directory
|
||||
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
|
||||
|
||||
set(libgav1_root "${libgav1_jni_root}/libgav1")
|
||||
set(libgav1_build "${libgav1_jni_build}/libgav1")
|
||||
|
||||
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
|
||||
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
|
||||
|
||||
# Build cpu_features library.
|
||||
add_subdirectory("${cpu_features_root}"
|
||||
"${cpu_features_build}"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1.
|
||||
add_subdirectory("${libgav1_root}"
|
||||
"${libgav1_build}"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1JNI.
|
||||
add_library(gav1JNI
|
||||
SHARED
|
||||
gav1_jni.cc)
|
||||
|
||||
# Locate NDK log library.
|
||||
find_library(android_log_lib log)
|
||||
|
||||
# Link libgav1JNI against used libraries.
|
||||
target_link_libraries(gav1JNI
|
||||
PRIVATE android
|
||||
PRIVATE cpu_features
|
||||
PRIVATE libgav1_static
|
||||
PRIVATE ${android_log_lib})
|
||||
|
||||
# Specify output directory for libgav1JNI.
|
||||
set_target_properties(gav1JNI PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY
|
||||
${libgav1_jni_output_directory})
|
754
extensions/av1/src/main/jni/gav1_jni.cc
Normal file
@ -0,0 +1,754 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
|
||||
#include <android/log.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
|
||||
#include "cpu_features_macros.h" // NOLINT
|
||||
#ifdef CPU_FEATURES_ARCH_ARM
|
||||
#include "cpuinfo_arm.h" // NOLINT
|
||||
#endif // CPU_FEATURES_ARCH_ARM
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
#include <arm_neon.h>
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <mutex> // NOLINT
|
||||
#include <new>
|
||||
|
||||
#include "gav1/decoder.h"
|
||||
|
||||
#define LOG_TAG "gav1_jni"
|
||||
#define LOGE(...) \
|
||||
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
|
||||
|
||||
#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_av1_Gav1Decoder_##NAME( \
|
||||
JNIEnv* env, jobject thiz, ##__VA_ARGS__); \
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_av1_Gav1Decoder_##NAME( \
|
||||
JNIEnv* env, jobject thiz, ##__VA_ARGS__)
|
||||
|
||||
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
JNIEnv* env;
|
||||
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return -1;
|
||||
}
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// YUV plane indices.
|
||||
const int kPlaneY = 0;
|
||||
const int kPlaneU = 1;
|
||||
const int kPlaneV = 2;
|
||||
const int kMaxPlanes = 3;
|
||||
|
||||
// Android YUV format. See:
|
||||
// https://developer.android.com/reference/android/graphics/ImageFormat.html#YV12.
|
||||
const int kImageFormatYV12 = 0x32315659;
|
||||
|
||||
// LINT.IfChange
|
||||
// Output modes.
|
||||
const int kOutputModeYuv = 0;
|
||||
const int kOutputModeSurfaceYuv = 1;
|
||||
// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/C.java)
|
||||
|
||||
// LINT.IfChange
|
||||
const int kColorSpaceUnknown = 0;
|
||||
// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java)
|
||||
|
||||
// LINT.IfChange
|
||||
// Return codes for jni methods.
|
||||
const int kStatusError = 0;
|
||||
const int kStatusOk = 1;
|
||||
const int kStatusDecodeOnly = 2;
|
||||
// LINT.ThenChange(../java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java)
|
||||
|
||||
// Status codes specific to the JNI wrapper code.
|
||||
enum JniStatusCode {
|
||||
kJniStatusOk = 0,
|
||||
kJniStatusOutOfMemory = -1,
|
||||
kJniStatusBufferAlreadyReleased = -2,
|
||||
kJniStatusInvalidNumOfPlanes = -3,
|
||||
kJniStatusBitDepth12NotSupportedWithYuv = -4,
|
||||
kJniStatusHighBitDepthNotSupportedWithSurfaceYuv = -5,
|
||||
kJniStatusANativeWindowError = -6,
|
||||
kJniStatusBufferResizeError = -7,
|
||||
kJniStatusNeonNotSupported = -8
|
||||
};
|
||||
|
||||
const char* GetJniErrorMessage(JniStatusCode error_code) {
|
||||
switch (error_code) {
|
||||
case kJniStatusOutOfMemory:
|
||||
return "Out of memory.";
|
||||
case kJniStatusBufferAlreadyReleased:
|
||||
return "JNI buffer already released.";
|
||||
case kJniStatusBitDepth12NotSupportedWithYuv:
|
||||
return "Bit depth 12 is not supported with YUV.";
|
||||
case kJniStatusHighBitDepthNotSupportedWithSurfaceYuv:
|
||||
return "High bit depth (10 or 12 bits per pixel) output format is not "
|
||||
"supported with YUV surface.";
|
||||
case kJniStatusInvalidNumOfPlanes:
|
||||
return "Libgav1 decoded buffer has invalid number of planes.";
|
||||
case kJniStatusANativeWindowError:
|
||||
return "ANativeWindow error.";
|
||||
case kJniStatusBufferResizeError:
|
||||
return "Buffer resize failed.";
|
||||
case kJniStatusNeonNotSupported:
|
||||
return "Neon is not supported.";
|
||||
default:
|
||||
return "Unrecognized error code.";
|
||||
}
|
||||
}
|
||||
|
||||
// Manages Libgav1FrameBuffer and reference information.
|
||||
class JniFrameBuffer {
|
||||
public:
|
||||
explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {
|
||||
gav1_frame_buffer_.private_data = &id_;
|
||||
}
|
||||
~JniFrameBuffer() {
|
||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||
delete[] gav1_frame_buffer_.data[plane_index];
|
||||
}
|
||||
}
|
||||
|
||||
void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) {
|
||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes();
|
||||
plane_index++) {
|
||||
stride_[plane_index] = decoder_buffer.stride[plane_index];
|
||||
plane_[plane_index] = decoder_buffer.plane[plane_index];
|
||||
displayed_width_[plane_index] =
|
||||
decoder_buffer.displayed_width[plane_index];
|
||||
displayed_height_[plane_index] =
|
||||
decoder_buffer.displayed_height[plane_index];
|
||||
}
|
||||
}
|
||||
|
||||
int Stride(int plane_index) const { return stride_[plane_index]; }
|
||||
uint8_t* Plane(int plane_index) const { return plane_[plane_index]; }
|
||||
int DisplayedWidth(int plane_index) const {
|
||||
return displayed_width_[plane_index];
|
||||
}
|
||||
int DisplayedHeight(int plane_index) const {
|
||||
return displayed_height_[plane_index];
|
||||
}
|
||||
|
||||
// Methods maintaining reference count are not thread-safe. They must be
|
||||
// called with a lock held.
|
||||
void AddReference() { ++reference_count_; }
|
||||
void RemoveReference() { reference_count_--; }
|
||||
bool InUse() const { return reference_count_ != 0; }
|
||||
|
||||
const Libgav1FrameBuffer& GetGav1FrameBuffer() const {
|
||||
return gav1_frame_buffer_;
|
||||
}
|
||||
|
||||
// Attempts to reallocate data planes if the existing ones don't have enough
|
||||
// capacity. Returns true if the allocation was successful or wasn't needed,
|
||||
// false if the allocation failed.
|
||||
bool MaybeReallocateGav1DataPlanes(int y_plane_min_size,
|
||||
int uv_plane_min_size) {
|
||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||
const int min_size =
|
||||
(plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size;
|
||||
if (gav1_frame_buffer_.size[plane_index] >= min_size) continue;
|
||||
delete[] gav1_frame_buffer_.data[plane_index];
|
||||
gav1_frame_buffer_.data[plane_index] =
|
||||
new (std::nothrow) uint8_t[min_size];
|
||||
if (!gav1_frame_buffer_.data[plane_index]) {
|
||||
gav1_frame_buffer_.size[plane_index] = 0;
|
||||
return false;
|
||||
}
|
||||
gav1_frame_buffer_.size[plane_index] = min_size;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
int stride_[kMaxPlanes];
|
||||
uint8_t* plane_[kMaxPlanes];
|
||||
int displayed_width_[kMaxPlanes];
|
||||
int displayed_height_[kMaxPlanes];
|
||||
int id_;
|
||||
int reference_count_;
|
||||
Libgav1FrameBuffer gav1_frame_buffer_ = {};
|
||||
};
|
||||
|
||||
// Manages frame buffers used by libgav1 decoder and ExoPlayer.
|
||||
// Handles synchronization between libgav1 and ExoPlayer threads.
|
||||
class JniBufferManager {
|
||||
public:
|
||||
~JniBufferManager() {
|
||||
// This lock does not do anything since libgav1 has released all the frame
|
||||
// buffers. It exists to merely be consistent with all other usage of
|
||||
// |all_buffers_| and |all_buffer_count_|.
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
while (all_buffer_count_--) {
|
||||
delete all_buffers_[all_buffer_count_];
|
||||
}
|
||||
}
|
||||
|
||||
JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size,
|
||||
Libgav1FrameBuffer* frame_buffer) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
JniFrameBuffer* output_buffer;
|
||||
if (free_buffer_count_) {
|
||||
output_buffer = free_buffers_[--free_buffer_count_];
|
||||
} else if (all_buffer_count_ < kMaxFrames) {
|
||||
output_buffer = new (std::nothrow) JniFrameBuffer(all_buffer_count_);
|
||||
if (output_buffer == nullptr) return kJniStatusOutOfMemory;
|
||||
all_buffers_[all_buffer_count_++] = output_buffer;
|
||||
} else {
|
||||
// Maximum number of buffers is being used.
|
||||
return kJniStatusOutOfMemory;
|
||||
}
|
||||
if (!output_buffer->MaybeReallocateGav1DataPlanes(y_plane_min_size,
|
||||
uv_plane_min_size)) {
|
||||
return kJniStatusOutOfMemory;
|
||||
}
|
||||
|
||||
output_buffer->AddReference();
|
||||
*frame_buffer = output_buffer->GetGav1FrameBuffer();
|
||||
|
||||
return kJniStatusOk;
|
||||
}
|
||||
|
||||
JniFrameBuffer* GetBuffer(int id) const { return all_buffers_[id]; }
|
||||
|
||||
void AddBufferReference(int id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
all_buffers_[id]->AddReference();
|
||||
}
|
||||
|
||||
JniStatusCode ReleaseBuffer(int id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
JniFrameBuffer* buffer = all_buffers_[id];
|
||||
if (!buffer->InUse()) {
|
||||
return kJniStatusBufferAlreadyReleased;
|
||||
}
|
||||
buffer->RemoveReference();
|
||||
if (!buffer->InUse()) {
|
||||
free_buffers_[free_buffer_count_++] = buffer;
|
||||
}
|
||||
return kJniStatusOk;
|
||||
}
|
||||
|
||||
private:
|
||||
static const int kMaxFrames = 32;
|
||||
|
||||
JniFrameBuffer* all_buffers_[kMaxFrames];
|
||||
int all_buffer_count_ = 0;
|
||||
|
||||
JniFrameBuffer* free_buffers_[kMaxFrames];
|
||||
int free_buffer_count_ = 0;
|
||||
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
struct JniContext {
|
||||
~JniContext() {
|
||||
if (native_window) {
|
||||
ANativeWindow_release(native_window);
|
||||
}
|
||||
}
|
||||
|
||||
bool MaybeAcquireNativeWindow(JNIEnv* env, jobject new_surface) {
|
||||
if (surface == new_surface) {
|
||||
return true;
|
||||
}
|
||||
if (native_window) {
|
||||
ANativeWindow_release(native_window);
|
||||
}
|
||||
native_window_width = 0;
|
||||
native_window_height = 0;
|
||||
native_window = ANativeWindow_fromSurface(env, new_surface);
|
||||
if (native_window == nullptr) {
|
||||
jni_status_code = kJniStatusANativeWindowError;
|
||||
surface = nullptr;
|
||||
return false;
|
||||
}
|
||||
surface = new_surface;
|
||||
return true;
|
||||
}
|
||||
|
||||
jfieldID decoder_private_field;
|
||||
jfieldID output_mode_field;
|
||||
jfieldID data_field;
|
||||
jmethodID init_for_private_frame_method;
|
||||
jmethodID init_for_yuv_frame_method;
|
||||
|
||||
JniBufferManager buffer_manager;
|
||||
// The libgav1 decoder instance has to be deleted before |buffer_manager| is
|
||||
// destructed. This will make sure that libgav1 releases all the frame
|
||||
// buffers that it might be holding references to. So this has to be declared
|
||||
// after |buffer_manager| since the destruction happens in reverse order of
|
||||
// declaration.
|
||||
libgav1::Decoder decoder;
|
||||
|
||||
ANativeWindow* native_window = nullptr;
|
||||
jobject surface = nullptr;
|
||||
int native_window_width = 0;
|
||||
int native_window_height = 0;
|
||||
|
||||
Libgav1StatusCode libgav1_status_code = kLibgav1StatusOk;
|
||||
JniStatusCode jni_status_code = kJniStatusOk;
|
||||
};
|
||||
|
||||
int Libgav1GetFrameBuffer(void* private_data, size_t y_plane_min_size,
|
||||
size_t uv_plane_min_size,
|
||||
Libgav1FrameBuffer* frame_buffer) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(private_data);
|
||||
context->jni_status_code = context->buffer_manager.GetBuffer(
|
||||
y_plane_min_size, uv_plane_min_size, frame_buffer);
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Libgav1ReleaseFrameBuffer(void* private_data,
|
||||
Libgav1FrameBuffer* frame_buffer) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(private_data);
|
||||
const int buffer_id = *reinterpret_cast<int*>(frame_buffer->private_data);
|
||||
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr int AlignTo16(int value) { return (value + 15) & (~15); }
|
||||
|
||||
void CopyPlane(const uint8_t* source, int source_stride, uint8_t* destination,
|
||||
int destination_stride, int width, int height) {
|
||||
while (height--) {
|
||||
std::memcpy(destination, source, width);
|
||||
source += source_stride;
|
||||
destination += destination_stride;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyFrameToDataBuffer(const libgav1::DecoderBuffer* decoder_buffer,
|
||||
jbyte* data) {
|
||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer->NumPlanes();
|
||||
plane_index++) {
|
||||
const uint64_t length = decoder_buffer->stride[plane_index] *
|
||||
decoder_buffer->displayed_height[plane_index];
|
||||
memcpy(data, decoder_buffer->plane[plane_index], length);
|
||||
data += length;
|
||||
}
|
||||
}
|
||||
|
||||
void Convert10BitFrameTo8BitDataBuffer(
|
||||
const libgav1::DecoderBuffer* decoder_buffer, jbyte* data) {
|
||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer->NumPlanes();
|
||||
plane_index++) {
|
||||
int sample = 0;
|
||||
const uint8_t* source = decoder_buffer->plane[plane_index];
|
||||
for (int i = 0; i < decoder_buffer->displayed_height[plane_index]; i++) {
|
||||
const uint16_t* source_16 = reinterpret_cast<const uint16_t*>(source);
|
||||
for (int j = 0; j < decoder_buffer->displayed_width[plane_index]; j++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sample += source_16[j];
|
||||
data[j] = sample >> 2;
|
||||
sample &= 3; // Remainder.
|
||||
}
|
||||
source += decoder_buffer->stride[plane_index];
|
||||
data += decoder_buffer->stride[plane_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
void Convert10BitFrameTo8BitDataBufferNeon(
|
||||
const libgav1::DecoderBuffer* decoder_buffer, jbyte* data) {
|
||||
uint32x2_t lcg_value = vdup_n_u32(random());
|
||||
lcg_value = vset_lane_u32(random(), lcg_value, 1);
|
||||
// LCG values recommended in "Numerical Recipes".
|
||||
const uint32x2_t LCG_MULT = vdup_n_u32(1664525);
|
||||
const uint32x2_t LCG_INCR = vdup_n_u32(1013904223);
|
||||
|
||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||
const uint8_t* source = decoder_buffer->plane[plane_index];
|
||||
|
||||
for (int i = 0; i < decoder_buffer->displayed_height[plane_index]; i++) {
|
||||
const uint16_t* source_16 = reinterpret_cast<const uint16_t*>(source);
|
||||
uint8_t* destination = reinterpret_cast<uint8_t*>(data);
|
||||
|
||||
// Each read consumes 4 2-byte samples, but to reduce branches and
|
||||
// random steps we unroll to 4 rounds, so each loop consumes 16
|
||||
// samples.
|
||||
const int j_max = decoder_buffer->displayed_width[plane_index] & ~15;
|
||||
int j;
|
||||
for (j = 0; j < j_max; j += 16) {
|
||||
// Run a round of the RNG.
|
||||
lcg_value = vmla_u32(LCG_INCR, lcg_value, LCG_MULT);
|
||||
|
||||
// Round 1.
|
||||
// The lower two bits of this LCG parameterization are garbage,
|
||||
// leaving streaks on the image. We access the upper bits of each
|
||||
// 16-bit lane by shifting. (We use this both as an 8- and 16-bit
|
||||
// vector, so the choice of which one to keep it as is arbitrary.)
|
||||
uint8x8_t randvec =
|
||||
vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_value), 8));
|
||||
|
||||
// We retrieve the values and shift them so that the bits we'll
|
||||
// shift out (after biasing) are in the upper 8 bits of each 16-bit
|
||||
// lane.
|
||||
uint16x4_t values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
// We add the bias bits in the lower 8 to the shifted values to get
|
||||
// the final values in the upper 8 bits.
|
||||
uint16x4_t added_1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
// Round 2.
|
||||
// Shifting the randvec bits left by 2 bits, as an 8-bit vector,
|
||||
// should leave us with enough bias to get the needed rounding
|
||||
// operation.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
|
||||
// Retrieve and sum the next 4 pixels.
|
||||
values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
uint16x4_t added_2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
// Reinterpret the two added vectors as 8x8, zip them together, and
|
||||
// discard the lower portions.
|
||||
uint8x8_t zipped =
|
||||
vuzp_u8(vreinterpret_u8_u16(added_1), vreinterpret_u8_u16(added_2))
|
||||
.val[1];
|
||||
vst1_u8(destination, zipped);
|
||||
destination += 8;
|
||||
|
||||
// Run it again with the next two rounds using the remaining
|
||||
// entropy in randvec.
|
||||
|
||||
// Round 3.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
added_1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
// Round 4.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
added_2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
zipped =
|
||||
vuzp_u8(vreinterpret_u8_u16(added_1), vreinterpret_u8_u16(added_2))
|
||||
.val[1];
|
||||
vst1_u8(destination, zipped);
|
||||
destination += 8;
|
||||
}
|
||||
|
||||
uint32_t randval = 0;
|
||||
// For the remaining pixels in each row - usually none, as most
|
||||
// standard sizes are divisible by 32 - convert them "by hand".
|
||||
for (; j < decoder_buffer->displayed_width[plane_index]; j++) {
|
||||
if (!randval) randval = random();
|
||||
destination[j] = (source_16[j] + (randval & 3)) >> 2;
|
||||
randval >>= 2;
|
||||
}
|
||||
|
||||
source += decoder_buffer->stride[plane_index];
|
||||
data += decoder_buffer->stride[plane_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
|
||||
} // namespace
|
||||
|
||||
DECODER_FUNC(jlong, gav1Init, jint threads) {
|
||||
JniContext* context = new (std::nothrow) JniContext();
|
||||
if (context == nullptr) {
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
#ifdef CPU_FEATURES_ARCH_ARM
|
||||
// Libgav1 requires NEON with arm ABIs.
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
const cpu_features::ArmFeatures arm_features =
|
||||
cpu_features::GetArmInfo().features;
|
||||
if (!arm_features.neon) {
|
||||
context->jni_status_code = kJniStatusNeonNotSupported;
|
||||
return reinterpret_cast<jlong>(context);
|
||||
}
|
||||
#else
|
||||
context->jni_status_code = kJniStatusNeonNotSupported;
|
||||
return reinterpret_cast<jlong>(context);
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
#endif // CPU_FEATURES_ARCH_ARM
|
||||
|
||||
libgav1::DecoderSettings settings;
|
||||
settings.threads = threads;
|
||||
settings.get = Libgav1GetFrameBuffer;
|
||||
settings.release = Libgav1ReleaseFrameBuffer;
|
||||
settings.callback_private_data = context;
|
||||
|
||||
context->libgav1_status_code = context->decoder.Init(&settings);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return reinterpret_cast<jlong>(context);
|
||||
}
|
||||
|
||||
// Populate JNI References.
|
||||
const jclass outputBufferClass = env->FindClass(
|
||||
"com/google/android/exoplayer2/video/VideoDecoderOutputBuffer");
|
||||
context->decoder_private_field =
|
||||
env->GetFieldID(outputBufferClass, "decoderPrivate", "I");
|
||||
context->output_mode_field = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||
context->data_field =
|
||||
env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;");
|
||||
context->init_for_private_frame_method =
|
||||
env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
|
||||
context->init_for_yuv_frame_method =
|
||||
env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z");
|
||||
|
||||
return reinterpret_cast<jlong>(context);
|
||||
}
|
||||
|
||||
DECODER_FUNC(void, gav1Close, jlong jContext) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
delete context;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData,
|
||||
jint length) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const uint8_t* const buffer = reinterpret_cast<const uint8_t*>(
|
||||
env->GetDirectBufferAddress(encodedData));
|
||||
context->libgav1_status_code =
|
||||
context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return kStatusError;
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer,
|
||||
jboolean decodeOnly) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const libgav1::DecoderBuffer* decoder_buffer;
|
||||
context->libgav1_status_code = context->decoder.DequeueFrame(&decoder_buffer);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
if (decodeOnly || decoder_buffer == nullptr) {
|
||||
// This is not an error. The input data was decode-only or no displayable
|
||||
// frames are available.
|
||||
return kStatusDecodeOnly;
|
||||
}
|
||||
|
||||
const int output_mode =
|
||||
env->GetIntField(jOutputBuffer, context->output_mode_field);
|
||||
if (output_mode == kOutputModeYuv) {
|
||||
// Resize the buffer if required. Default color conversion will be used as
|
||||
// libgav1::DecoderBuffer doesn't expose color space info.
|
||||
const jboolean init_result = env->CallBooleanMethod(
|
||||
jOutputBuffer, context->init_for_yuv_frame_method,
|
||||
decoder_buffer->displayed_width[kPlaneY],
|
||||
decoder_buffer->displayed_height[kPlaneY],
|
||||
decoder_buffer->stride[kPlaneY], decoder_buffer->stride[kPlaneU],
|
||||
kColorSpaceUnknown);
|
||||
if (env->ExceptionCheck()) {
|
||||
// Exception is thrown in Java when returning from the native call.
|
||||
return kStatusError;
|
||||
}
|
||||
if (!init_result) {
|
||||
context->jni_status_code = kJniStatusBufferResizeError;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
const jobject data_object =
|
||||
env->GetObjectField(jOutputBuffer, context->data_field);
|
||||
jbyte* const data =
|
||||
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(data_object));
|
||||
|
||||
switch (decoder_buffer->bitdepth) {
|
||||
case 8:
|
||||
CopyFrameToDataBuffer(decoder_buffer, data);
|
||||
break;
|
||||
case 10:
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
Convert10BitFrameTo8BitDataBufferNeon(decoder_buffer, data);
|
||||
#else
|
||||
Convert10BitFrameTo8BitDataBuffer(decoder_buffer, data);
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
break;
|
||||
default:
|
||||
context->jni_status_code = kJniStatusBitDepth12NotSupportedWithYuv;
|
||||
return kStatusError;
|
||||
}
|
||||
} else if (output_mode == kOutputModeSurfaceYuv) {
|
||||
if (decoder_buffer->bitdepth != 8) {
|
||||
context->jni_status_code =
|
||||
kJniStatusHighBitDepthNotSupportedWithSurfaceYuv;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
if (decoder_buffer->NumPlanes() > kMaxPlanes) {
|
||||
context->jni_status_code = kJniStatusInvalidNumOfPlanes;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
const int buffer_id =
|
||||
*reinterpret_cast<int*>(decoder_buffer->buffer_private_data);
|
||||
context->buffer_manager.AddBufferReference(buffer_id);
|
||||
JniFrameBuffer* const jni_buffer =
|
||||
context->buffer_manager.GetBuffer(buffer_id);
|
||||
jni_buffer->SetFrameData(*decoder_buffer);
|
||||
env->CallVoidMethod(jOutputBuffer, context->init_for_private_frame_method,
|
||||
decoder_buffer->displayed_width[kPlaneY],
|
||||
decoder_buffer->displayed_height[kPlaneY]);
|
||||
if (env->ExceptionCheck()) {
|
||||
// Exception is thrown in Java when returning from the native call.
|
||||
return kStatusError;
|
||||
}
|
||||
env->SetIntField(jOutputBuffer, context->decoder_private_field, buffer_id);
|
||||
}
|
||||
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1RenderFrame, jlong jContext, jobject jSurface,
|
||||
jobject jOutputBuffer) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const int buffer_id =
|
||||
env->GetIntField(jOutputBuffer, context->decoder_private_field);
|
||||
JniFrameBuffer* const jni_buffer =
|
||||
context->buffer_manager.GetBuffer(buffer_id);
|
||||
|
||||
if (!context->MaybeAcquireNativeWindow(env, jSurface)) {
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
if (context->native_window_width != jni_buffer->DisplayedWidth(kPlaneY) ||
|
||||
context->native_window_height != jni_buffer->DisplayedHeight(kPlaneY)) {
|
||||
if (ANativeWindow_setBuffersGeometry(
|
||||
context->native_window, jni_buffer->DisplayedWidth(kPlaneY),
|
||||
jni_buffer->DisplayedHeight(kPlaneY), kImageFormatYV12)) {
|
||||
context->jni_status_code = kJniStatusANativeWindowError;
|
||||
return kStatusError;
|
||||
}
|
||||
context->native_window_width = jni_buffer->DisplayedWidth(kPlaneY);
|
||||
context->native_window_height = jni_buffer->DisplayedHeight(kPlaneY);
|
||||
}
|
||||
|
||||
ANativeWindow_Buffer native_window_buffer;
|
||||
if (ANativeWindow_lock(context->native_window, &native_window_buffer,
|
||||
/*inOutDirtyBounds=*/nullptr) ||
|
||||
native_window_buffer.bits == nullptr) {
|
||||
context->jni_status_code = kJniStatusANativeWindowError;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
// Y plane
|
||||
CopyPlane(jni_buffer->Plane(kPlaneY), jni_buffer->Stride(kPlaneY),
|
||||
reinterpret_cast<uint8_t*>(native_window_buffer.bits),
|
||||
native_window_buffer.stride, jni_buffer->DisplayedWidth(kPlaneY),
|
||||
jni_buffer->DisplayedHeight(kPlaneY));
|
||||
|
||||
const int y_plane_size =
|
||||
native_window_buffer.stride * native_window_buffer.height;
|
||||
const int32_t native_window_buffer_uv_height =
|
||||
(native_window_buffer.height + 1) / 2;
|
||||
const int native_window_buffer_uv_stride =
|
||||
AlignTo16(native_window_buffer.stride / 2);
|
||||
|
||||
// TODO(b/140606738): Handle monochrome videos.
|
||||
|
||||
// V plane
|
||||
// Since the format for ANativeWindow is YV12, V plane is being processed
|
||||
// before U plane.
|
||||
const int v_plane_height = std::min(native_window_buffer_uv_height,
|
||||
jni_buffer->DisplayedHeight(kPlaneV));
|
||||
CopyPlane(
|
||||
jni_buffer->Plane(kPlaneV), jni_buffer->Stride(kPlaneV),
|
||||
reinterpret_cast<uint8_t*>(native_window_buffer.bits) + y_plane_size,
|
||||
native_window_buffer_uv_stride, jni_buffer->DisplayedWidth(kPlaneV),
|
||||
v_plane_height);
|
||||
|
||||
const int v_plane_size = v_plane_height * native_window_buffer_uv_stride;
|
||||
|
||||
// U plane
|
||||
CopyPlane(jni_buffer->Plane(kPlaneU), jni_buffer->Stride(kPlaneU),
|
||||
reinterpret_cast<uint8_t*>(native_window_buffer.bits) +
|
||||
y_plane_size + v_plane_size,
|
||||
native_window_buffer_uv_stride, jni_buffer->DisplayedWidth(kPlaneU),
|
||||
std::min(native_window_buffer_uv_height,
|
||||
jni_buffer->DisplayedHeight(kPlaneU)));
|
||||
|
||||
if (ANativeWindow_unlockAndPost(context->native_window)) {
|
||||
context->jni_status_code = kJniStatusANativeWindowError;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
DECODER_FUNC(void, gav1ReleaseFrame, jlong jContext, jobject jOutputBuffer) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const int buffer_id =
|
||||
env->GetIntField(jOutputBuffer, context->decoder_private_field);
|
||||
env->SetIntField(jOutputBuffer, context->decoder_private_field, -1);
|
||||
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||
}
|
||||
}
|
||||
|
||||
DECODER_FUNC(jstring, gav1GetErrorMessage, jlong jContext) {
|
||||
if (jContext == 0) {
|
||||
return env->NewStringUTF("Failed to initialize JNI context.");
|
||||
}
|
||||
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return env->NewStringUTF(
|
||||
libgav1::GetErrorString(context->libgav1_status_code));
|
||||
}
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
return env->NewStringUTF(GetJniErrorMessage(context->jni_status_code));
|
||||
}
|
||||
|
||||
return env->NewStringUTF("None.");
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1CheckError, jlong jContext) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk ||
|
||||
context->jni_status_code != kJniStatusOk) {
|
||||
return kStatusError;
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
// TODO(b/139902005): Add functions for getting libgav1 version and build
|
||||
// configuration once libgav1 ABI provides this information.
|
@ -32,12 +32,13 @@ android {
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -16,7 +16,6 @@
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.BasePlayer;
|
||||
import com.google.android.exoplayer2.C;
|
||||
@ -51,14 +50,14 @@ import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||
*
|
||||
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
||||
* Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link
|
||||
* #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||
* implemented and attached to the player.
|
||||
* injected {@link CastContext}. To keep track of the session, {@link #isCastSessionAvailable()} can
|
||||
* be queried and {@link SessionAvailabilityListener} can be implemented and attached to the player.
|
||||
*
|
||||
* <p>If no session is available, the player state will remain unchanged and calls to methods that
|
||||
* alter it will be ignored. Querying the player state is possible even when no session is
|
||||
@ -99,14 +98,14 @@ public final class CastPlayer extends BasePlayer {
|
||||
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
private final StateHolder<Boolean> playWhenReady;
|
||||
private final StateHolder<Integer> repeatMode;
|
||||
@Nullable private RemoteMediaClient remoteMediaClient;
|
||||
private CastTimeline currentTimeline;
|
||||
private TrackGroupArray currentTrackGroups;
|
||||
private TrackSelectionArray currentTrackSelection;
|
||||
private int playbackState;
|
||||
private int repeatMode;
|
||||
@Player.State private int playbackState;
|
||||
private int currentWindowIndex;
|
||||
private boolean playWhenReady;
|
||||
private long lastReportedPositionMs;
|
||||
private int pendingSeekCount;
|
||||
private int pendingSeekWindowIndex;
|
||||
@ -126,19 +125,20 @@ public final class CastPlayer extends BasePlayer {
|
||||
notificationsBatch = new ArrayList<>();
|
||||
ongoingNotificationsTasks = new ArrayDeque<>();
|
||||
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
CastSession session = sessionManager.getCurrentCastSession();
|
||||
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
|
||||
|
||||
playWhenReady = new StateHolder<>(false);
|
||||
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
|
||||
playbackState = STATE_IDLE;
|
||||
repeatMode = REPEAT_MODE_OFF;
|
||||
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
currentTrackGroups = TrackGroupArray.EMPTY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
updateInternalState();
|
||||
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
CastSession session = sessionManager.getCurrentCastSession();
|
||||
setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
}
|
||||
|
||||
// Media Queue manipulation methods.
|
||||
@ -328,6 +328,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Player.State
|
||||
public int getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
@ -349,16 +350,29 @@ public final class CastPlayer extends BasePlayer {
|
||||
if (remoteMediaClient == null) {
|
||||
return;
|
||||
}
|
||||
if (playWhenReady) {
|
||||
remoteMediaClient.play();
|
||||
} else {
|
||||
remoteMediaClient.pause();
|
||||
}
|
||||
// We update the local state and send the message to the receiver app, which will cause the
|
||||
// operation to be perceived as synchronous by the user. When the operation reports a result,
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setPlayerStateAndNotifyIfChanged(playWhenReady, playbackState);
|
||||
flushNotifications();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
|
||||
this.playWhenReady.pendingResultCallback =
|
||||
new ResultCallback<MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult mediaChannelResult) {
|
||||
if (remoteMediaClient != null) {
|
||||
updatePlayerStateAndNotifyIfChanged(this);
|
||||
flushNotifications();
|
||||
}
|
||||
}
|
||||
};
|
||||
pendingResult.setResultCallback(this.playWhenReady.pendingResultCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return playWhenReady;
|
||||
return playWhenReady.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -434,14 +448,32 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
|
||||
if (remoteMediaClient == null) {
|
||||
return;
|
||||
}
|
||||
// We update the local state and send the message to the receiver app, which will cause the
|
||||
// operation to be perceived as synchronous by the user. When the operation reports a result,
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setRepeatModeAndNotifyIfChanged(repeatMode);
|
||||
flushNotifications();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
|
||||
this.repeatMode.pendingResultCallback =
|
||||
new ResultCallback<MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult mediaChannelResult) {
|
||||
if (remoteMediaClient != null) {
|
||||
updateRepeatModeAndNotifyIfChanged(this);
|
||||
flushNotifications();
|
||||
}
|
||||
}
|
||||
};
|
||||
pendingResult.setResultCallback(this.repeatMode.pendingResultCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RepeatMode public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
return repeatMode.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -470,11 +502,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
return currentTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable public Object getCurrentManifest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
return getCurrentWindowIndex();
|
||||
@ -547,35 +574,20 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void updateInternalState() {
|
||||
private void updateInternalStateAndNotifyIfChanged() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return;
|
||||
}
|
||||
|
||||
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady;
|
||||
int playbackState = fetchPlaybackState(remoteMediaClient);
|
||||
boolean playWhenReady = !remoteMediaClient.isPaused();
|
||||
if (this.playbackState != playbackState
|
||||
|| this.playWhenReady != playWhenReady) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady = playWhenReady;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
|
||||
}
|
||||
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady;
|
||||
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
if (wasPlaying != isPlaying) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
|
||||
}
|
||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
|
||||
}
|
||||
maybeUpdateTimelineAndNotify();
|
||||
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
|
||||
int currentWindowIndex = C.INDEX_UNSET;
|
||||
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
|
||||
@ -593,7 +605,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
listener ->
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
|
||||
}
|
||||
if (updateTracksAndSelections()) {
|
||||
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
|
||||
@ -601,15 +613,43 @@ public final class CastPlayer extends BasePlayer {
|
||||
flushNotifications();
|
||||
}
|
||||
|
||||
private void maybeUpdateTimelineAndNotify() {
|
||||
/**
|
||||
* Updates {@link #playWhenReady} and {@link #playbackState} to match the Cast {@code
|
||||
* remoteMediaClient} state, and notifies listeners of any state changes.
|
||||
*
|
||||
* <p>This method will only update values whose {@link StateHolder#pendingResultCallback} matches
|
||||
* the given {@code resultCallback}.
|
||||
*/
|
||||
@RequiresNonNull("remoteMediaClient")
|
||||
private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
|
||||
boolean newPlayWhenReadyValue = playWhenReady.value;
|
||||
if (playWhenReady.acceptsUpdate(resultCallback)) {
|
||||
newPlayWhenReadyValue = !remoteMediaClient.isPaused();
|
||||
playWhenReady.clearPendingResultCallback();
|
||||
}
|
||||
// We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
|
||||
setPlayerStateAndNotifyIfChanged(newPlayWhenReadyValue, fetchPlaybackState(remoteMediaClient));
|
||||
}
|
||||
|
||||
@RequiresNonNull("remoteMediaClient")
|
||||
private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
|
||||
if (repeatMode.acceptsUpdate(resultCallback)) {
|
||||
setRepeatModeAndNotifyIfChanged(fetchRepeatMode(remoteMediaClient));
|
||||
repeatMode.clearPendingResultCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimelineAndNotifyIfChanged() {
|
||||
if (updateTimeline()) {
|
||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
@Player.TimelineChangeReason
|
||||
int reason =
|
||||
waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED
|
||||
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
waitingForInitialTimeline = false;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason)));
|
||||
listener -> listener.onTimelineChanged(currentTimeline, reason)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -626,10 +666,8 @@ public final class CastPlayer extends BasePlayer {
|
||||
return !oldTimeline.equals(currentTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal tracks and selection and returns whether they have changed.
|
||||
*/
|
||||
private boolean updateTracksAndSelections() {
|
||||
/** Updates the internal tracks and selection and returns whether they have changed. */
|
||||
private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return false;
|
||||
@ -675,6 +713,25 @@ public final class CastPlayer extends BasePlayer {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
|
||||
if (this.repeatMode.value != repeatMode) {
|
||||
this.repeatMode.value = repeatMode;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
|
||||
}
|
||||
}
|
||||
|
||||
private void setPlayerStateAndNotifyIfChanged(
|
||||
boolean playWhenReady, @Player.State int playbackState) {
|
||||
if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) {
|
||||
this.playWhenReady.value = playWhenReady;
|
||||
this.playbackState = playbackState;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)));
|
||||
}
|
||||
}
|
||||
|
||||
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
|
||||
if (this.remoteMediaClient == remoteMediaClient) {
|
||||
// Do nothing.
|
||||
@ -691,7 +748,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
remoteMediaClient.addListener(statusListener);
|
||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||
updateInternalState();
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
} else {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||
@ -778,8 +835,26 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatusListener implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
|
||||
private void flushNotifications() {
|
||||
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
||||
ongoingNotificationsTasks.addAll(notificationsBatch);
|
||||
notificationsBatch.clear();
|
||||
if (recursiveNotification) {
|
||||
// This will be handled once the current notification task is finished.
|
||||
return;
|
||||
}
|
||||
while (!ongoingNotificationsTasks.isEmpty()) {
|
||||
ongoingNotificationsTasks.peekFirst().execute();
|
||||
ongoingNotificationsTasks.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private final class StatusListener
|
||||
implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>,
|
||||
RemoteMediaClient.ProgressListener {
|
||||
|
||||
// RemoteMediaClient.ProgressListener implementation.
|
||||
|
||||
@ -792,7 +867,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
updateInternalState();
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -800,7 +875,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public void onQueueStatusUpdated() {
|
||||
maybeUpdateTimelineAndNotify();
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -863,28 +938,10 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void flushNotifications() {
|
||||
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
||||
ongoingNotificationsTasks.addAll(notificationsBatch);
|
||||
notificationsBatch.clear();
|
||||
if (recursiveNotification) {
|
||||
// This will be handled once the current notification task is finished.
|
||||
return;
|
||||
}
|
||||
while (!ongoingNotificationsTasks.isEmpty()) {
|
||||
ongoingNotificationsTasks.peekFirst().execute();
|
||||
ongoingNotificationsTasks.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(@NonNull MediaChannelResult result) {
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
|
||||
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
|
||||
@ -899,6 +956,42 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds the value and the masking status of a specific part of the {@link CastPlayer} state. */
|
||||
private static final class StateHolder<T> {
|
||||
|
||||
/** The user-facing value of a specific part of the {@link CastPlayer} state. */
|
||||
public T value;
|
||||
|
||||
/**
|
||||
* If {@link #value} is being masked, holds the result callback for the operation that triggered
|
||||
* the masking. Or null if {@link #value} is not being masked.
|
||||
*/
|
||||
@Nullable public ResultCallback<MediaChannelResult> pendingResultCallback;
|
||||
|
||||
public StateHolder(T initialValue) {
|
||||
value = initialValue;
|
||||
}
|
||||
|
||||
public void clearPendingResultCallback() {
|
||||
pendingResultCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this state holder accepts updates coming from the given result callback.
|
||||
*
|
||||
* <p>A null {@code resultCallback} means that the update is a regular receiver state update, in
|
||||
* which case the update will only be accepted if {@link #value} is not being masked. If {@link
|
||||
* #value} is being masked, the update will only be accepted if {@code resultCallback} is the
|
||||
* same as the {@link #pendingResultCallback}.
|
||||
*
|
||||
* @param resultCallback A result callback. May be null if the update comes from a regular
|
||||
* receiver status update.
|
||||
*/
|
||||
public boolean acceptsUpdate(@Nullable ResultCallback<?> resultCallback) {
|
||||
return pendingResultCallback == resultCallback;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListenerNotificationTask {
|
||||
|
||||
private final Iterator<ListenerHolder> listenersSnapshot;
|
||||
@ -915,5 +1008,4 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import java.util.Arrays;
|
||||
@ -39,9 +39,14 @@ import java.util.Arrays;
|
||||
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
|
||||
*/
|
||||
public final long defaultPositionUs;
|
||||
/** Whether the item is live content, or {@code false} if unknown. */
|
||||
public final boolean isLive;
|
||||
|
||||
private ItemData() {
|
||||
this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);
|
||||
this(
|
||||
/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
|
||||
C.TIME_UNSET,
|
||||
/* isLive= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,26 +54,29 @@ import java.util.Arrays;
|
||||
*
|
||||
* @param durationUs See {@link #durationsUs}.
|
||||
* @param defaultPositionUs See {@link #defaultPositionUs}.
|
||||
* @param isLive See {@link #isLive}.
|
||||
*/
|
||||
public ItemData(long durationUs, long defaultPositionUs) {
|
||||
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
|
||||
this.durationUs = durationUs;
|
||||
this.defaultPositionUs = defaultPositionUs;
|
||||
this.isLive = isLive;
|
||||
}
|
||||
|
||||
/** Returns an instance with the given {@link #durationsUs}. */
|
||||
public ItemData copyWithDurationUs(long durationUs) {
|
||||
if (durationUs == this.durationUs) {
|
||||
/**
|
||||
* Returns a copy of this instance with the given values.
|
||||
*
|
||||
* @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
|
||||
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
|
||||
* if unknown.
|
||||
* @param isLive Whether the item is live, or {@code false} if unknown.
|
||||
*/
|
||||
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
|
||||
if (durationUs == this.durationUs
|
||||
&& defaultPositionUs == this.defaultPositionUs
|
||||
&& isLive == this.isLive) {
|
||||
return this;
|
||||
}
|
||||
return new ItemData(durationUs, defaultPositionUs);
|
||||
}
|
||||
|
||||
/** Returns an instance with the given {@link #defaultPositionsUs}. */
|
||||
public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {
|
||||
if (defaultPositionUs == this.defaultPositionUs) {
|
||||
return this;
|
||||
}
|
||||
return new ItemData(durationUs, defaultPositionUs);
|
||||
return new ItemData(durationUs, defaultPositionUs, isLive);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +88,7 @@ import java.util.Arrays;
|
||||
private final int[] ids;
|
||||
private final long[] durationsUs;
|
||||
private final long[] defaultPositionsUs;
|
||||
private final boolean[] isLive;
|
||||
|
||||
/**
|
||||
* Creates a Cast timeline from the given data.
|
||||
@ -93,12 +102,14 @@ import java.util.Arrays;
|
||||
ids = Arrays.copyOf(itemIds, itemCount);
|
||||
durationsUs = new long[itemCount];
|
||||
defaultPositionsUs = new long[itemCount];
|
||||
isLive = new boolean[itemCount];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
int id = ids[i];
|
||||
idsToIndex.put(id, i);
|
||||
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
|
||||
durationsUs[i] = data.durationUs;
|
||||
defaultPositionsUs[i] = data.defaultPositionUs;
|
||||
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
|
||||
isLive[i] = data.isLive;
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,17 +121,18 @@ import java.util.Arrays;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(
|
||||
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
|
||||
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
|
||||
long durationUs = durationsUs[windowIndex];
|
||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
Object tag = setTag ? ids[windowIndex] : null;
|
||||
return window.set(
|
||||
tag,
|
||||
/* uid= */ ids[windowIndex],
|
||||
/* tag= */ ids[windowIndex],
|
||||
/* manifest= */ null,
|
||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||
/* isSeekable= */ !isDynamic,
|
||||
isDynamic,
|
||||
isLive[windowIndex],
|
||||
defaultPositionsUs[windowIndex],
|
||||
durationUs,
|
||||
/* firstPeriodIndex= */ windowIndex,
|
||||
@ -161,7 +173,8 @@ import java.util.Arrays;
|
||||
CastTimeline that = (CastTimeline) other;
|
||||
return Arrays.equals(ids, that.ids)
|
||||
&& Arrays.equals(durationsUs, that.durationsUs)
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
|
||||
&& Arrays.equals(isLive, that.isLive);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -169,6 +182,7 @@ import java.util.Arrays;
|
||||
int result = Arrays.hashCode(ids);
|
||||
result = 31 * result + Arrays.hashCode(durationsUs);
|
||||
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
|
||||
result = 31 * result + Arrays.hashCode(isLive);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,9 @@
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
@ -61,25 +63,33 @@ import java.util.HashSet;
|
||||
}
|
||||
|
||||
int currentItemId = mediaStatus.getCurrentItemId();
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());
|
||||
itemIdToData.put(
|
||||
currentItemId,
|
||||
itemIdToData
|
||||
.get(currentItemId, CastTimeline.ItemData.EMPTY)
|
||||
.copyWithDurationUs(durationUs));
|
||||
updateItemData(
|
||||
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
|
||||
|
||||
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
|
||||
int itemId = item.getItemId();
|
||||
itemIdToData.put(
|
||||
itemId,
|
||||
itemIdToData
|
||||
.get(itemId, CastTimeline.ItemData.EMPTY)
|
||||
.copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
|
||||
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
||||
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
|
||||
}
|
||||
|
||||
return new CastTimeline(itemIds, itemIdToData);
|
||||
}
|
||||
|
||||
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
|
||||
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
||||
if (durationUs == C.TIME_UNSET) {
|
||||
durationUs = previousData.durationUs;
|
||||
}
|
||||
boolean isLive =
|
||||
mediaInfo == null
|
||||
? previousData.isLive
|
||||
: mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE;
|
||||
if (defaultPositionUs == C.TIME_UNSET) {
|
||||
defaultPositionUs = previousData.defaultPositionUs;
|
||||
}
|
||||
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
|
||||
}
|
||||
|
||||
private void removeUnusedItemDataEntries(int[] itemIds) {
|
||||
HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
|
||||
for (int id : itemIds) {
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack;
|
||||
* @param mediaInfo The media info to get the duration from.
|
||||
* @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
|
||||
*/
|
||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||
public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
|
||||
if (mediaInfo == null) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
@ -28,11 +28,33 @@ import java.util.List;
|
||||
*/
|
||||
public final class DefaultCastOptionsProvider implements OptionsProvider {
|
||||
|
||||
/**
|
||||
* App id of the Default Media Receiver app. Apps that do not require DRM support may use this
|
||||
* receiver receiver app ID.
|
||||
*
|
||||
* <p>See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
|
||||
*/
|
||||
public static final String APP_ID_DEFAULT_RECEIVER =
|
||||
CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
|
||||
|
||||
/**
|
||||
* App id for receiver app with rudimentary support for DRM.
|
||||
*
|
||||
* <p>This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
|
||||
* production use. In order to use DRM, custom receiver apps should be used. For environments that
|
||||
* do not require DRM, the default receiver app should be used (see {@link
|
||||
* #APP_ID_DEFAULT_RECEIVER}).
|
||||
*/
|
||||
// TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
|
||||
// b/128603245].
|
||||
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
|
||||
|
||||
@Override
|
||||
public CastOptions getCastOptions(Context context) {
|
||||
return new CastOptions.Builder()
|
||||
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
|
||||
.setStopReceiverApplicationWhenEndingSession(true).build();
|
||||
.setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
|
||||
.setStopReceiverApplicationWhenEndingSession(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.UUID;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Default {@link MediaItemConverter} implementation. */
|
||||
public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
|
||||
private static final String KEY_MEDIA_ITEM = "mediaItem";
|
||||
private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
|
||||
private static final String KEY_URI = "uri";
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_MIME_TYPE = "mimeType";
|
||||
private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
|
||||
private static final String KEY_UUID = "uuid";
|
||||
private static final String KEY_LICENSE_URI = "licenseUri";
|
||||
private static final String KEY_REQUEST_HEADERS = "requestHeaders";
|
||||
|
||||
@Override
|
||||
public MediaItem toMediaItem(MediaQueueItem item) {
|
||||
return getMediaItem(item.getMedia().getCustomData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaQueueItem toMediaQueueItem(MediaItem item) {
|
||||
if (item.mimeType == null) {
|
||||
throw new IllegalArgumentException("The item must specify its mimeType");
|
||||
}
|
||||
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
if (item.title != null) {
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
||||
}
|
||||
MediaInfo mediaInfo =
|
||||
new MediaInfo.Builder(item.uri.toString())
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setContentType(item.mimeType)
|
||||
.setMetadata(metadata)
|
||||
.setCustomData(getCustomData(item))
|
||||
.build();
|
||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
||||
}
|
||||
|
||||
// Deserialization.
|
||||
|
||||
private static MediaItem getMediaItem(JSONObject customData) {
|
||||
try {
|
||||
JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
|
||||
if (mediaItemJson.has(KEY_TITLE)) {
|
||||
builder.setTitle(mediaItemJson.getString(KEY_TITLE));
|
||||
}
|
||||
if (mediaItemJson.has(KEY_MIME_TYPE)) {
|
||||
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
|
||||
}
|
||||
if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
|
||||
builder.setDrmConfiguration(
|
||||
getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
|
||||
}
|
||||
return builder.build();
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
|
||||
UUID uuid = UUID.fromString(json.getString(KEY_UUID));
|
||||
Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
|
||||
JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
|
||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
||||
for (Iterator<String> iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
|
||||
String key = iterator.next();
|
||||
requestHeaders.put(key, requestHeadersJson.getString(key));
|
||||
}
|
||||
return new DrmConfiguration(uuid, licenseUri, requestHeaders);
|
||||
}
|
||||
|
||||
// Serialization.
|
||||
|
||||
private static JSONObject getCustomData(MediaItem item) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
|
||||
JSONObject playerConfigJson = getPlayerConfigJson(item);
|
||||
if (playerConfigJson != null) {
|
||||
json.put(KEY_PLAYER_CONFIG, playerConfigJson);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(KEY_URI, item.uri.toString());
|
||||
json.put(KEY_TITLE, item.title);
|
||||
json.put(KEY_MIME_TYPE, item.mimeType);
|
||||
if (item.drmConfiguration != null) {
|
||||
json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
|
||||
throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(KEY_UUID, drmConfiguration.uuid);
|
||||
json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri);
|
||||
json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders));
|
||||
return json;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
|
||||
DrmConfiguration drmConfiguration = item.drmConfiguration;
|
||||
if (drmConfiguration == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String drmScheme;
|
||||
if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
|
||||
drmScheme = "widevine";
|
||||
} else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) {
|
||||
drmScheme = "playready";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject exoPlayerConfigJson = new JSONObject();
|
||||
exoPlayerConfigJson.put("withCredentials", false);
|
||||
exoPlayerConfigJson.put("protectionSystem", drmScheme);
|
||||
if (drmConfiguration.licenseUri != null) {
|
||||
exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri);
|
||||
}
|
||||
if (!drmConfiguration.requestHeaders.isEmpty()) {
|
||||
exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders));
|
||||
}
|
||||
|
||||
return exoPlayerConfigJson;
|
||||
}
|
||||
}
|
@ -17,42 +17,31 @@ package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
|
||||
/** Representation of an item that can be played by a media player. */
|
||||
/** Representation of a media item. */
|
||||
public final class MediaItem {
|
||||
|
||||
/** A builder for {@link MediaItem} instances. */
|
||||
public static final class Builder {
|
||||
|
||||
@Nullable private UUID uuid;
|
||||
private String title;
|
||||
private String description;
|
||||
private MediaItem.UriBundle media;
|
||||
@Nullable private Object attachment;
|
||||
private List<MediaItem.DrmScheme> drmSchemes;
|
||||
private long startPositionUs;
|
||||
private long endPositionUs;
|
||||
private String mimeType;
|
||||
@Nullable private Uri uri;
|
||||
@Nullable private String title;
|
||||
@Nullable private String mimeType;
|
||||
@Nullable private DrmConfiguration drmConfiguration;
|
||||
|
||||
/** Creates an builder with default field values. */
|
||||
public Builder() {
|
||||
clearInternal();
|
||||
/** See {@link MediaItem#uri}. */
|
||||
public Builder setUri(String uri) {
|
||||
return setUri(Uri.parse(uri));
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#uuid}. */
|
||||
public Builder setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
/** See {@link MediaItem#uri}. */
|
||||
public Builder setUri(Uri uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -62,307 +51,125 @@ public final class MediaItem {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#description}. */
|
||||
public Builder setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */
|
||||
public Builder setMedia(String uri) {
|
||||
return setMedia(new UriBundle(Uri.parse(uri)));
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#media}. */
|
||||
public Builder setMedia(UriBundle media) {
|
||||
this.media = media;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#attachment}. */
|
||||
public Builder setAttachment(Object attachment) {
|
||||
this.attachment = attachment;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#drmSchemes}. */
|
||||
public Builder setDrmSchemes(List<MediaItem.DrmScheme> drmSchemes) {
|
||||
this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes));
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#startPositionUs}. */
|
||||
public Builder setStartPositionUs(long startPositionUs) {
|
||||
this.startPositionUs = startPositionUs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#endPositionUs}. */
|
||||
public Builder setEndPositionUs(long endPositionUs) {
|
||||
Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE);
|
||||
this.endPositionUs = endPositionUs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#mimeType}. */
|
||||
public Builder setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the
|
||||
* {@link MediaItem}.
|
||||
*/
|
||||
public MediaItem buildAndClear() {
|
||||
MediaItem item = build();
|
||||
clearInternal();
|
||||
return item;
|
||||
}
|
||||
|
||||
/** Returns the builder to default values. */
|
||||
public Builder clear() {
|
||||
clearInternal();
|
||||
/** See {@link MediaItem#drmConfiguration}. */
|
||||
public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
|
||||
this.drmConfiguration = drmConfiguration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MediaItem} instance with the current builder values. This method also
|
||||
* clears any values passed to {@link #setUuid(UUID)}.
|
||||
*/
|
||||
/** Returns a new {@link MediaItem} instance with the current builder values. */
|
||||
public MediaItem build() {
|
||||
UUID uuid = this.uuid;
|
||||
this.uuid = null;
|
||||
return new MediaItem(
|
||||
uuid != null ? uuid : UUID.randomUUID(),
|
||||
title,
|
||||
description,
|
||||
media,
|
||||
attachment,
|
||||
drmSchemes,
|
||||
startPositionUs,
|
||||
endPositionUs,
|
||||
mimeType);
|
||||
}
|
||||
|
||||
@EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"})
|
||||
private void clearInternal(@UnknownInitialization Builder this) {
|
||||
uuid = null;
|
||||
title = "";
|
||||
description = "";
|
||||
media = UriBundle.EMPTY;
|
||||
attachment = null;
|
||||
drmSchemes = Collections.emptyList();
|
||||
startPositionUs = C.TIME_UNSET;
|
||||
endPositionUs = C.TIME_UNSET;
|
||||
mimeType = "";
|
||||
Assertions.checkNotNull(uri);
|
||||
return new MediaItem(uri, title, mimeType, drmConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/** Bundles a resource's URI with headers to attach to any request to that URI. */
|
||||
public static final class UriBundle {
|
||||
|
||||
/** An empty {@link UriBundle}. */
|
||||
public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY);
|
||||
|
||||
/** A URI. */
|
||||
public final Uri uri;
|
||||
|
||||
/** The headers to attach to any request for the given URI. */
|
||||
public final Map<String, String> requestHeaders;
|
||||
|
||||
/**
|
||||
* Creates an instance with no request headers.
|
||||
*
|
||||
* @param uri See {@link #uri}.
|
||||
*/
|
||||
public UriBundle(Uri uri) {
|
||||
this(uri, Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance with the given URI and request headers.
|
||||
*
|
||||
* @param uri See {@link #uri}.
|
||||
* @param requestHeaders See {@link #requestHeaders}.
|
||||
*/
|
||||
public UriBundle(Uri uri, Map<String, String> requestHeaders) {
|
||||
this.uri = uri;
|
||||
this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UriBundle uriBundle = (UriBundle) other;
|
||||
return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = uri.hashCode();
|
||||
result = 31 * result + requestHeaders.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a DRM protection scheme, and optionally provides information about how to acquire
|
||||
* the license for the media.
|
||||
*/
|
||||
public static final class DrmScheme {
|
||||
/** DRM configuration for a media item. */
|
||||
public static final class DrmConfiguration {
|
||||
|
||||
/** The UUID of the protection scheme. */
|
||||
public final UUID uuid;
|
||||
|
||||
/**
|
||||
* Optional {@link UriBundle} for the license server. If no license server is provided, the
|
||||
* server must be provided by the media.
|
||||
* Optional license server {@link Uri}. If {@code null} then the license server must be
|
||||
* specified by the media.
|
||||
*/
|
||||
@Nullable public final UriBundle licenseServer;
|
||||
@Nullable public final Uri licenseUri;
|
||||
|
||||
/** Headers that should be attached to any license requests. */
|
||||
public final Map<String, String> requestHeaders;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param uuid See {@link #uuid}.
|
||||
* @param licenseServer See {@link #licenseServer}.
|
||||
* @param licenseUri See {@link #licenseUri}.
|
||||
* @param requestHeaders See {@link #requestHeaders}.
|
||||
*/
|
||||
public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {
|
||||
public DrmConfiguration(
|
||||
UUID uuid, @Nullable Uri licenseUri, @Nullable Map<String, String> requestHeaders) {
|
||||
this.uuid = uuid;
|
||||
this.licenseServer = licenseServer;
|
||||
this.licenseUri = licenseUri;
|
||||
this.requestHeaders =
|
||||
requestHeaders == null
|
||||
? Collections.emptyMap()
|
||||
: Collections.unmodifiableMap(requestHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DrmScheme drmScheme = (DrmScheme) other;
|
||||
return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);
|
||||
DrmConfiguration other = (DrmConfiguration) obj;
|
||||
return uuid.equals(other.uuid)
|
||||
&& Util.areEqual(licenseUri, other.licenseUri)
|
||||
&& requestHeaders.equals(other.requestHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = uuid.hashCode();
|
||||
result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);
|
||||
result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
|
||||
result = 31 * result + requestHeaders.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A UUID that identifies this item, potentially across different devices. The default value is
|
||||
* obtained by calling {@link UUID#randomUUID()}.
|
||||
*/
|
||||
public final UUID uuid;
|
||||
/** The media {@link Uri}. */
|
||||
public final Uri uri;
|
||||
|
||||
/** The title of the item. The default value is an empty string. */
|
||||
public final String title;
|
||||
/** The title of the item, or {@code null} if unspecified. */
|
||||
@Nullable public final String title;
|
||||
|
||||
/** A description for the item. The default value is an empty string. */
|
||||
public final String description;
|
||||
/** The mime type for the media, or {@code null} if unspecified. */
|
||||
@Nullable public final String mimeType;
|
||||
|
||||
/**
|
||||
* A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}.
|
||||
*/
|
||||
public final UriBundle media;
|
||||
/** Optional {@link DrmConfiguration} for the media. */
|
||||
@Nullable public final DrmConfiguration drmConfiguration;
|
||||
|
||||
/**
|
||||
* An optional opaque object to attach to the media item. Handling of this attachment is
|
||||
* implementation specific. The default value is null.
|
||||
*/
|
||||
@Nullable public final Object attachment;
|
||||
|
||||
/**
|
||||
* Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The
|
||||
* default value is an empty list.
|
||||
*/
|
||||
public final List<DrmScheme> drmSchemes;
|
||||
|
||||
/**
|
||||
* The position in microseconds at which playback of this media item should start. {@link
|
||||
* C#TIME_UNSET} if playback should start at the default position. The default value is {@link
|
||||
* C#TIME_UNSET}.
|
||||
*/
|
||||
public final long startPositionUs;
|
||||
|
||||
/**
|
||||
* The position in microseconds at which playback of this media item should end. {@link
|
||||
* C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link
|
||||
* C#TIME_UNSET}.
|
||||
*/
|
||||
public final long endPositionUs;
|
||||
|
||||
/**
|
||||
* The mime type of this media item. The default value is an empty string.
|
||||
*
|
||||
* <p>The usage of this mime type is optional and player implementation specific.
|
||||
*/
|
||||
public final String mimeType;
|
||||
|
||||
// TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.
|
||||
private MediaItem(
|
||||
Uri uri,
|
||||
@Nullable String title,
|
||||
@Nullable String mimeType,
|
||||
@Nullable DrmConfiguration drmConfiguration) {
|
||||
this.uri = uri;
|
||||
this.title = title;
|
||||
this.mimeType = mimeType;
|
||||
this.drmConfiguration = drmConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MediaItem mediaItem = (MediaItem) other;
|
||||
return startPositionUs == mediaItem.startPositionUs
|
||||
&& endPositionUs == mediaItem.endPositionUs
|
||||
&& uuid.equals(mediaItem.uuid)
|
||||
&& title.equals(mediaItem.title)
|
||||
&& description.equals(mediaItem.description)
|
||||
&& media.equals(mediaItem.media)
|
||||
&& Util.areEqual(attachment, mediaItem.attachment)
|
||||
&& drmSchemes.equals(mediaItem.drmSchemes)
|
||||
&& mimeType.equals(mediaItem.mimeType);
|
||||
MediaItem other = (MediaItem) obj;
|
||||
return uri.equals(other.uri)
|
||||
&& Util.areEqual(title, other.title)
|
||||
&& Util.areEqual(mimeType, other.mimeType)
|
||||
&& Util.areEqual(drmConfiguration, other.drmConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = uuid.hashCode();
|
||||
result = 31 * result + title.hashCode();
|
||||
result = 31 * result + description.hashCode();
|
||||
result = 31 * result + media.hashCode();
|
||||
result = 31 * result + (attachment != null ? attachment.hashCode() : 0);
|
||||
result = 31 * result + drmSchemes.hashCode();
|
||||
result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));
|
||||
result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));
|
||||
result = 31 * result + mimeType.hashCode();
|
||||
int result = uri.hashCode();
|
||||
result = 31 * result + (title == null ? 0 : title.hashCode());
|
||||
result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
|
||||
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
private MediaItem(
|
||||
UUID uuid,
|
||||
String title,
|
||||
String description,
|
||||
UriBundle media,
|
||||
@Nullable Object attachment,
|
||||
List<DrmScheme> drmSchemes,
|
||||
long startPositionUs,
|
||||
long endPositionUs,
|
||||
String mimeType) {
|
||||
this.uuid = uuid;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.media = media;
|
||||
this.attachment = attachment;
|
||||
this.drmSchemes = drmSchemes;
|
||||
this.startPositionUs = startPositionUs;
|
||||
this.endPositionUs = endPositionUs;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
|
||||
/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
|
||||
public interface MediaItemConverter {
|
||||
|
||||
/**
|
||||
* Converts a {@link MediaItem} to a {@link MediaQueueItem}.
|
||||
*
|
||||
* @param mediaItem The {@link MediaItem}.
|
||||
* @return An equivalent {@link MediaQueueItem}.
|
||||
*/
|
||||
MediaQueueItem toMediaQueueItem(MediaItem mediaItem);
|
||||
|
||||
/**
|
||||
* Converts a {@link MediaQueueItem} to a {@link MediaItem}.
|
||||
*
|
||||
* @param mediaQueueItem The {@link MediaQueueItem}.
|
||||
* @return The equivalent {@link MediaItem}.
|
||||
*/
|
||||
MediaItem toMediaItem(MediaQueueItem mediaQueueItem);
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
/** Represents a sequence of {@link MediaItem MediaItems}. */
|
||||
public interface MediaItemQueue {
|
||||
|
||||
/**
|
||||
* Returns the item at the given index.
|
||||
*
|
||||
* @param index The index of the item to retrieve.
|
||||
* @return The item at the given index.
|
||||
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
|
||||
*/
|
||||
MediaItem get(int index);
|
||||
|
||||
/** Returns the number of items in this queue. */
|
||||
int getSize();
|
||||
|
||||
/**
|
||||
* Appends the given sequence of items to the queue.
|
||||
*
|
||||
* @param items The sequence of items to append.
|
||||
*/
|
||||
void add(MediaItem... items);
|
||||
|
||||
/**
|
||||
* Adds the given sequence of items to the queue at the given position, so that the first of
|
||||
* {@code items} is placed at the given index.
|
||||
*
|
||||
* @param index The index at which {@code items} will be inserted.
|
||||
* @param items The sequence of items to append.
|
||||
* @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
|
||||
*/
|
||||
void add(int index, MediaItem... items);
|
||||
|
||||
/**
|
||||
* Moves an existing item within the playlist.
|
||||
*
|
||||
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
|
||||
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
|
||||
* moment of the invocation, playback will stick with the moved item.
|
||||
*
|
||||
* @param indexFrom The index of the item to move.
|
||||
* @param indexTo The index at which the item will be placed after this operation.
|
||||
* @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
|
||||
*/
|
||||
void move(int indexFrom, int indexTo);
|
||||
|
||||
/**
|
||||
* Removes an item from the queue.
|
||||
*
|
||||
* @param index The index of the item to remove from the queue.
|
||||
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
|
||||
*/
|
||||
void remove(int index);
|
||||
|
||||
/**
|
||||
* Removes a range of items from the queue.
|
||||
*
|
||||
* <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
|
||||
*
|
||||
* @param from The inclusive index at which the range to remove starts.
|
||||
* @param exclusiveTo The exclusive index at which the range to remove ends.
|
||||
* @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
|
||||
* exclusiveTo}.
|
||||
*/
|
||||
void removeRange(int from, int exclusiveTo);
|
||||
|
||||
/** Removes all items in the queue. */
|
||||
void clear();
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -14,4 +14,6 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.cast.test"/>
|
||||
<manifest package="com.google.android.exoplayer2.ext.cast.test">
|
||||
<uses-sdk/>
|
||||
</manifest>
|
||||
|
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.cast.framework.CastSession;
|
||||
import com.google.android.gms.cast.framework.SessionManager;
|
||||
import com.google.android.gms.cast.framework.media.MediaQueue;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/** Tests for {@link CastPlayer}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class CastPlayerTest {
|
||||
|
||||
private CastPlayer castPlayer;
|
||||
private RemoteMediaClient.Listener remoteMediaClientListener;
|
||||
@Mock private RemoteMediaClient mockRemoteMediaClient;
|
||||
@Mock private MediaStatus mockMediaStatus;
|
||||
@Mock private MediaQueue mockMediaQueue;
|
||||
@Mock private CastContext mockCastContext;
|
||||
@Mock private SessionManager mockSessionManager;
|
||||
@Mock private CastSession mockCastSession;
|
||||
@Mock private Player.EventListener mockListener;
|
||||
@Mock private PendingResult<RemoteMediaClient.MediaChannelResult> mockPendingResult;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
|
||||
setResultCallbackArgumentCaptor;
|
||||
|
||||
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
initMocks(this);
|
||||
when(mockCastContext.getSessionManager()).thenReturn(mockSessionManager);
|
||||
when(mockSessionManager.getCurrentCastSession()).thenReturn(mockCastSession);
|
||||
when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
|
||||
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
|
||||
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
|
||||
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
|
||||
// Make the remote media client present the same default values as ExoPlayer:
|
||||
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
|
||||
castPlayer = new CastPlayer(mockCastContext);
|
||||
castPlayer.addListener(mockListener);
|
||||
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
|
||||
remoteMediaClientListener = listenerArgumentCaptor.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPlayWhenReady_masksRemoteState() {
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
|
||||
castPlayer.setPlayWhenReady(true);
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the remoteMediaClient has updated its state according to the play() call.
|
||||
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
||||
setResultCallbackArgumentCaptor
|
||||
.getValue()
|
||||
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
|
||||
castPlayer.setPlayWhenReady(true);
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
|
||||
// Upon result, the remote media client is still paused. The state should reflect that.
|
||||
setResultCallbackArgumentCaptor
|
||||
.getValue()
|
||||
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlayWhenReady_changesOnStatusUpdates() {
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetRepeatMode_masksRemoteState() {
|
||||
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||
|
||||
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
|
||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the mediaStatus now exposes the new repeat mode.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
|
||||
setResultCallbackArgumentCaptor
|
||||
.getValue()
|
||||
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetRepeatMode_updatesUponResultChange() {
|
||||
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
||||
|
||||
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
|
||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the repeat mode is ALL. The state should reflect that.
|
||||
setResultCallbackArgumentCaptor
|
||||
.getValue()
|
||||
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatMode_changesOnStatusUpdates() {
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.Collections;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Test for {@link DefaultMediaItemConverter}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DefaultMediaItemConverterTest {
|
||||
|
||||
@Test
|
||||
public void serialize_deserialize_minimal() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
|
||||
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
|
||||
MediaItem reconstructedItem = converter.toMediaItem(queueItem);
|
||||
|
||||
assertThat(reconstructedItem).isEqualTo(item);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serialize_deserialize_complete() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem item =
|
||||
builder
|
||||
.setUri(Uri.parse("http://example.com"))
|
||||
.setTitle("title")
|
||||
.setMimeType("mime")
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("http://license.com"),
|
||||
Collections.singletonMap("key", "value")))
|
||||
.build();
|
||||
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
|
||||
MediaItem reconstructedItem = converter.toMediaItem(queueItem);
|
||||
|
||||
assertThat(reconstructedItem).isEqualTo(item);
|
||||
}
|
||||
}
|
@ -21,10 +21,7 @@ import android.net.Uri;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@ -32,113 +29,58 @@ import org.junit.runner.RunWith;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MediaItemTest {
|
||||
|
||||
@Test
|
||||
public void buildMediaItem_resetsUuid() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
UUID uuid = new UUID(1, 1);
|
||||
MediaItem item1 = builder.setUuid(uuid).build();
|
||||
MediaItem item2 = builder.build();
|
||||
MediaItem item3 = builder.build();
|
||||
assertThat(item1.uuid).isEqualTo(uuid);
|
||||
assertThat(item2.uuid).isNotEqualTo(uuid);
|
||||
assertThat(item3.uuid).isNotEqualTo(item2.uuid);
|
||||
assertThat(item3.uuid).isNotEqualTo(uuid);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildMediaItem_doesNotChangeState() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem item1 =
|
||||
builder
|
||||
.setUuid(new UUID(0, 1))
|
||||
.setMedia("http://example.com")
|
||||
.setUri(Uri.parse("http://example.com"))
|
||||
.setTitle("title")
|
||||
.setMimeType(MimeTypes.AUDIO_MP4)
|
||||
.setStartPositionUs(3)
|
||||
.setEndPositionUs(4)
|
||||
.build();
|
||||
MediaItem item2 = builder.setUuid(new UUID(0, 1)).build();
|
||||
MediaItem item2 = builder.build();
|
||||
assertThat(item1).isEqualTo(item2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildMediaItem_assertDefaultValues() {
|
||||
assertDefaultValues(new MediaItem.Builder().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildAndClear_assertDefaultValues() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
builder
|
||||
.setMedia("http://example.com")
|
||||
.setTitle("title")
|
||||
.setMimeType(MimeTypes.AUDIO_MP4)
|
||||
.setStartPositionUs(3)
|
||||
.setEndPositionUs(4)
|
||||
.buildAndClear();
|
||||
assertDefaultValues(builder.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withEqualDrmSchemes_returnsTrue() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem.Builder builder1 = new MediaItem.Builder();
|
||||
MediaItem mediaItem1 =
|
||||
builder
|
||||
.setUuid(new UUID(0, 1))
|
||||
.setMedia("www.google.com")
|
||||
.setDrmSchemes(createDummyDrmSchemes(1))
|
||||
.buildAndClear();
|
||||
builder1
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||
.build();
|
||||
MediaItem.Builder builder2 = new MediaItem.Builder();
|
||||
MediaItem mediaItem2 =
|
||||
builder
|
||||
.setUuid(new UUID(0, 1))
|
||||
.setMedia("www.google.com")
|
||||
.setDrmSchemes(createDummyDrmSchemes(1))
|
||||
.buildAndClear();
|
||||
builder2
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||
.build();
|
||||
assertThat(mediaItem1).isEqualTo(mediaItem2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem.Builder builder1 = new MediaItem.Builder();
|
||||
MediaItem mediaItem1 =
|
||||
builder
|
||||
.setUuid(new UUID(0, 1))
|
||||
.setMedia("www.google.com")
|
||||
.setDrmSchemes(createDummyDrmSchemes(1))
|
||||
.buildAndClear();
|
||||
builder1
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||
.build();
|
||||
MediaItem.Builder builder2 = new MediaItem.Builder();
|
||||
MediaItem mediaItem2 =
|
||||
builder
|
||||
.setUuid(new UUID(0, 1))
|
||||
.setMedia("www.google.com")
|
||||
.setDrmSchemes(createDummyDrmSchemes(2))
|
||||
.buildAndClear();
|
||||
builder2
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(2))
|
||||
.build();
|
||||
assertThat(mediaItem1).isNotEqualTo(mediaItem2);
|
||||
}
|
||||
|
||||
private static void assertDefaultValues(MediaItem item) {
|
||||
assertThat(item.title).isEmpty();
|
||||
assertThat(item.description).isEmpty();
|
||||
assertThat(item.media.uri).isEqualTo(Uri.EMPTY);
|
||||
assertThat(item.attachment).isNull();
|
||||
assertThat(item.drmSchemes).isEmpty();
|
||||
assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);
|
||||
assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);
|
||||
assertThat(item.mimeType).isEmpty();
|
||||
}
|
||||
|
||||
private static List<MediaItem.DrmScheme> createDummyDrmSchemes(int seed) {
|
||||
HashMap<String, String> requestHeaders1 = new HashMap<>();
|
||||
requestHeaders1.put("key1", "value1");
|
||||
requestHeaders1.put("key2", "value1");
|
||||
MediaItem.UriBundle uriBundle1 =
|
||||
new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
|
||||
MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
|
||||
HashMap<String, String> requestHeaders2 = new HashMap<>();
|
||||
requestHeaders2.put("key3", "value3");
|
||||
requestHeaders2.put("key4", "valueWithSeed" + seed);
|
||||
MediaItem.UriBundle uriBundle2 =
|
||||
new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
|
||||
MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
|
||||
return Arrays.asList(drmScheme1, drmScheme2);
|
||||
private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
|
||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
||||
requestHeaders.put("key1", "value1");
|
||||
requestHeaders.put("key2", "value2" + seed);
|
||||
return new MediaItem.DrmConfiguration(
|
||||
C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
|
||||
}
|
||||
}
|
||||
|
@ -31,11 +31,13 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'org.chromium.net:cronet-embedded:75.3770.101'
|
||||
api 'org.chromium.net:cronet-embedded:76.3809.111'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -15,12 +15,13 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
|
||||
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
@ -35,12 +36,14 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||
import org.chromium.net.CronetEngine;
|
||||
import org.chromium.net.CronetException;
|
||||
import org.chromium.net.NetworkException;
|
||||
@ -51,7 +54,9 @@ import org.chromium.net.UrlResponseInfo;
|
||||
/**
|
||||
* DataSource without intermediate buffer based on Cronet API set using UrlRequest.
|
||||
*
|
||||
* <p>This class's methods are organized in the sequence of expected calls.
|
||||
* <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
|
||||
* priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
|
||||
* construct the instance.
|
||||
*/
|
||||
public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
@ -113,16 +118,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
private final CronetEngine cronetEngine;
|
||||
private final Executor executor;
|
||||
@Nullable private final Predicate<String> contentTypePredicate;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
private final boolean handleSetCookieRequests;
|
||||
private final RequestProperties defaultRequestProperties;
|
||||
@Nullable private final RequestProperties defaultRequestProperties;
|
||||
private final RequestProperties requestProperties;
|
||||
private final ConditionVariable operation;
|
||||
private final Clock clock;
|
||||
|
||||
@Nullable private Predicate<String> contentTypePredicate;
|
||||
|
||||
// Accessed by the calling thread only.
|
||||
private boolean opened;
|
||||
private long bytesToSkip;
|
||||
@ -130,18 +136,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
|
||||
// to reads made by the Cronet thread.
|
||||
private UrlRequest currentUrlRequest;
|
||||
private DataSpec currentDataSpec;
|
||||
@Nullable private UrlRequest currentUrlRequest;
|
||||
@Nullable private DataSpec currentDataSpec;
|
||||
|
||||
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
|
||||
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
|
||||
// thread.
|
||||
private ByteBuffer readBuffer;
|
||||
@Nullable private ByteBuffer readBuffer;
|
||||
|
||||
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
|
||||
// made by the calling thread.
|
||||
private UrlResponseInfo responseInfo;
|
||||
private IOException exception;
|
||||
@Nullable private UrlResponseInfo responseInfo;
|
||||
@Nullable private IOException exception;
|
||||
private boolean finished;
|
||||
|
||||
private volatile long currentConnectTimeoutMs;
|
||||
@ -155,7 +161,78 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* handling is a fast operation when using a direct executor.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
|
||||
this(cronetEngine, executor, /* contentTypePredicate= */ null);
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
/* resetTimeoutOnRedirects= */ false,
|
||||
/* defaultRequestProperties= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||
* handling is a fast operation when using a direct executor.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||
* server as HTTP headers on every request.
|
||||
*/
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
@Nullable RequestProperties defaultRequestProperties) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
Clock.DEFAULT,
|
||||
defaultRequestProperties,
|
||||
/* handleSetCookieRequests= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||
* handling is a fast operation when using a direct executor.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||
* server as HTTP headers on every request.
|
||||
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||
* the redirect url in the "Cookie" header.
|
||||
*/
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
@Nullable RequestProperties defaultRequestProperties,
|
||||
boolean handleSetCookieRequests) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
Clock.DEFAULT,
|
||||
defaultRequestProperties,
|
||||
handleSetCookieRequests);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,7 +245,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* #open(DataSpec)}.
|
||||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
|
||||
* #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
@ -179,9 +259,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
contentTypePredicate,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
false,
|
||||
null,
|
||||
false);
|
||||
/* resetTimeoutOnRedirects= */ false,
|
||||
/* defaultRequestProperties= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,8 +276,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param defaultRequestProperties The default request properties to be used.
|
||||
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||
* server as HTTP headers on every request.
|
||||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
@ -206,7 +289,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
@Nullable RequestProperties defaultRequestProperties) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
@ -214,9 +297,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
Clock.DEFAULT,
|
||||
defaultRequestProperties,
|
||||
false);
|
||||
/* handleSetCookieRequests= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,10 +314,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param defaultRequestProperties The default request properties to be used.
|
||||
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||
* server as HTTP headers on every request.
|
||||
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||
* the redirect url in the "Cookie" header.
|
||||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||
* RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
@ -243,35 +329,33 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties,
|
||||
@Nullable RequestProperties defaultRequestProperties,
|
||||
boolean handleSetCookieRequests) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
Clock.DEFAULT,
|
||||
defaultRequestProperties,
|
||||
handleSetCookieRequests);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
}
|
||||
|
||||
/* package */ CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
Clock clock,
|
||||
RequestProperties defaultRequestProperties,
|
||||
@Nullable RequestProperties defaultRequestProperties,
|
||||
boolean handleSetCookieRequests) {
|
||||
super(/* isNetwork= */ true);
|
||||
this.urlRequestCallback = new UrlRequestCallback();
|
||||
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
||||
this.executor = Assertions.checkNotNull(executor);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
@ -282,6 +366,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
operation = new ConditionVariable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
|
||||
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
||||
*
|
||||
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
|
||||
* predicate that was previously set.
|
||||
*/
|
||||
public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
}
|
||||
|
||||
// HttpDataSource implementation.
|
||||
|
||||
@Override
|
||||
@ -312,6 +407,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
|
||||
}
|
||||
@ -324,22 +420,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
operation.close();
|
||||
resetConnectTimeout();
|
||||
currentDataSpec = dataSpec;
|
||||
UrlRequest urlRequest;
|
||||
try {
|
||||
currentUrlRequest = buildRequestBuilder(dataSpec).build();
|
||||
urlRequest = buildRequestBuilder(dataSpec).build();
|
||||
currentUrlRequest = urlRequest;
|
||||
} catch (IOException e) {
|
||||
throw new OpenException(e, currentDataSpec, Status.IDLE);
|
||||
throw new OpenException(e, dataSpec, Status.IDLE);
|
||||
}
|
||||
currentUrlRequest.start();
|
||||
urlRequest.start();
|
||||
|
||||
transferInitializing(dataSpec);
|
||||
try {
|
||||
boolean connectionOpened = blockUntilConnectTimeout();
|
||||
if (exception != null) {
|
||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
||||
throw new OpenException(exception, dataSpec, getStatus(urlRequest));
|
||||
} else if (!connectionOpened) {
|
||||
// The timeout was reached before the connection was opened.
|
||||
throw new OpenException(
|
||||
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
@ -347,6 +444,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
// Check for a valid response code.
|
||||
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
|
||||
int responseCode = responseInfo.getHttpStatusCode();
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
InvalidResponseCodeException exception =
|
||||
@ -354,7 +452,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
responseCode,
|
||||
responseInfo.getHttpStatusText(),
|
||||
responseInfo.getAllHeaders(),
|
||||
currentDataSpec);
|
||||
dataSpec);
|
||||
if (responseCode == 416) {
|
||||
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
||||
}
|
||||
@ -362,11 +460,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
// Check for a valid content type.
|
||||
Predicate<String> contentTypePredicate = this.contentTypePredicate;
|
||||
if (contentTypePredicate != null) {
|
||||
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
|
||||
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
|
||||
if (!contentTypePredicate.evaluate(contentType)) {
|
||||
throw new InvalidContentTypeException(contentType, currentDataSpec);
|
||||
if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
|
||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,7 +475,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
|
||||
// Calculate the content length.
|
||||
if (!getIsCompressed(responseInfo)) {
|
||||
if (!isCompressed(responseInfo)) {
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesRemaining = dataSpec.length;
|
||||
} else {
|
||||
@ -385,7 +484,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
} else {
|
||||
// If the response is compressed then the content length will be that of the compressed data
|
||||
// which isn't what we want. Always use the dataSpec length in this case.
|
||||
bytesRemaining = currentDataSpec.length;
|
||||
bytesRemaining = dataSpec.length;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
@ -404,37 +503,19 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
ByteBuffer readBuffer = this.readBuffer;
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
this.readBuffer = readBuffer;
|
||||
}
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
currentUrlRequest.read(readBuffer);
|
||||
try {
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
Thread.currentThread().interrupt();
|
||||
throw new HttpDataSourceException(
|
||||
new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
} catch (SocketTimeoutException e) {
|
||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
readInternal(castNonNull(readBuffer));
|
||||
|
||||
if (exception != null) {
|
||||
throw new HttpDataSourceException(exception, currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
} else if (finished) {
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
@ -459,6 +540,115 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
|
||||
* starting at {@code buffer.position()}. Advances the position of the buffer by the number of
|
||||
* bytes read and returns this length.
|
||||
*
|
||||
* <p>If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
|
||||
* buffer} should be ignored. If the exception has error code {@code
|
||||
* HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
|
||||
* after the method has returned. Thus the caller should not attempt to reuse the buffer.
|
||||
*
|
||||
* <p>If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
|
||||
* because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
|
||||
* returned. Otherwise, the call will block until at least one byte of data has been read and the
|
||||
* number of bytes read is returned.
|
||||
*
|
||||
* <p>Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
|
||||
* alternative read method with its backed array.
|
||||
*
|
||||
* @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
|
||||
* ByteBuffer.
|
||||
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
|
||||
* because the end of the opened range has been reached.
|
||||
* @throws HttpDataSourceException If an error occurs reading from the source.
|
||||
* @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
|
||||
*/
|
||||
public int read(ByteBuffer buffer) throws HttpDataSourceException {
|
||||
Assertions.checkState(opened);
|
||||
|
||||
if (!buffer.isDirect()) {
|
||||
throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
|
||||
}
|
||||
if (!buffer.hasRemaining()) {
|
||||
return 0;
|
||||
} else if (bytesRemaining == 0) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
int readLength = buffer.remaining();
|
||||
|
||||
if (readBuffer != null) {
|
||||
// Skip all the bytes we can from readBuffer if there are still bytes to skip.
|
||||
if (bytesToSkip != 0) {
|
||||
if (bytesToSkip >= readBuffer.remaining()) {
|
||||
bytesToSkip -= readBuffer.remaining();
|
||||
readBuffer.position(readBuffer.limit());
|
||||
} else {
|
||||
readBuffer.position(readBuffer.position() + (int) bytesToSkip);
|
||||
bytesToSkip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is existing data in the readBuffer, read as much as possible. Return if any read.
|
||||
int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
|
||||
if (copyBytes != 0) {
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
bytesRemaining -= copyBytes;
|
||||
}
|
||||
bytesTransferred(copyBytes);
|
||||
return copyBytes;
|
||||
}
|
||||
}
|
||||
|
||||
boolean readMore = true;
|
||||
while (readMore) {
|
||||
// If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
|
||||
// buffer. If we do not need to skip bytes, we may write to buffer directly.
|
||||
final boolean useCallerBuffer = bytesToSkip == 0;
|
||||
|
||||
operation.close();
|
||||
|
||||
if (!useCallerBuffer) {
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
} else {
|
||||
readBuffer.clear();
|
||||
}
|
||||
if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
|
||||
readBuffer.limit((int) bytesToSkip);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill buffer with more data from Cronet.
|
||||
readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
|
||||
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
Assertions.checkState(
|
||||
useCallerBuffer
|
||||
? readLength > buffer.remaining()
|
||||
: castNonNull(readBuffer).position() > 0);
|
||||
// If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
|
||||
if (useCallerBuffer) {
|
||||
readMore = false;
|
||||
} else {
|
||||
bytesToSkip -= castNonNull(readBuffer).position();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final int bytesRead = readLength - buffer.remaining();
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
bytesTransferred(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (currentUrlRequest != null) {
|
||||
@ -497,29 +687,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
cronetEngine
|
||||
.newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
|
||||
.allowDirectExecutor();
|
||||
|
||||
// Set the headers.
|
||||
boolean isContentTypeHeaderSet = false;
|
||||
Map<String, String> requestHeaders = new HashMap<>();
|
||||
if (defaultRequestProperties != null) {
|
||||
for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {
|
||||
String key = headerEntry.getKey();
|
||||
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
requestHeaders.putAll(defaultRequestProperties.getSnapshot());
|
||||
}
|
||||
Map<String, String> requestPropertiesSnapshot = requestProperties.getSnapshot();
|
||||
for (Entry<String, String> headerEntry : requestPropertiesSnapshot.entrySet()) {
|
||||
requestHeaders.putAll(requestProperties.getSnapshot());
|
||||
requestHeaders.putAll(dataSpec.httpRequestHeaders);
|
||||
|
||||
for (Entry<String, String> headerEntry : requestHeaders.entrySet()) {
|
||||
String key = headerEntry.getKey();
|
||||
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
String value = headerEntry.getValue();
|
||||
requestBuilder.addHeader(key, value);
|
||||
}
|
||||
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||
|
||||
if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
|
||||
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
||||
}
|
||||
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
|
||||
requestBuilder.addHeader(
|
||||
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
|
||||
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
|
||||
}
|
||||
|
||||
// Set the Range header.
|
||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||
StringBuilder rangeValue = new StringBuilder();
|
||||
@ -531,7 +717,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
requestBuilder.addHeader("Range", rangeValue.toString());
|
||||
}
|
||||
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
|
||||
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
|
||||
// (adjusting the code as necessary).
|
||||
// Force identity encoding unless gzip is allowed.
|
||||
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
||||
@ -560,7 +746,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
|
||||
}
|
||||
|
||||
private static boolean getIsCompressed(UrlResponseInfo info) {
|
||||
/**
|
||||
* Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
|
||||
* them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
|
||||
* the current {@code readBuffer} object so that it is not reused in the future.
|
||||
*
|
||||
* @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
|
||||
* @throws HttpDataSourceException If an error occurs reading from the source.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
|
||||
castNonNull(currentUrlRequest).read(buffer);
|
||||
try {
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// The operation is ongoing so replace buffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
if (buffer == readBuffer) {
|
||||
readBuffer = null;
|
||||
}
|
||||
Thread.currentThread().interrupt();
|
||||
throw new HttpDataSourceException(
|
||||
new InterruptedIOException(e),
|
||||
castNonNull(currentDataSpec),
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
} catch (SocketTimeoutException e) {
|
||||
// The operation is ongoing so replace buffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
if (buffer == readBuffer) {
|
||||
readBuffer = null;
|
||||
}
|
||||
throw new HttpDataSourceException(
|
||||
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new HttpDataSourceException(
|
||||
exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCompressed(UrlResponseInfo info) {
|
||||
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
|
||||
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
|
||||
return !entry.getValue().equalsIgnoreCase("identity");
|
||||
@ -638,10 +866,22 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
return statusHolder[0];
|
||||
}
|
||||
|
||||
private static boolean isEmpty(List<?> list) {
|
||||
@EnsuresNonNullIf(result = false, expression = "#1")
|
||||
private static boolean isEmpty(@Nullable List<?> list) {
|
||||
return list == null || list.isEmpty();
|
||||
}
|
||||
|
||||
// Copy as much as possible from the src buffer into dst buffer.
|
||||
// Returns the number of bytes copied.
|
||||
private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
|
||||
int remaining = Math.min(src.remaining(), dst.remaining());
|
||||
int limit = src.limit();
|
||||
src.limit(src.position() + remaining);
|
||||
dst.put(src);
|
||||
src.limit(limit);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
private final class UrlRequestCallback extends UrlRequest.Callback {
|
||||
|
||||
@Override
|
||||
@ -650,13 +890,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||
UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
|
||||
DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
|
||||
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||
int responseCode = info.getHttpStatusCode();
|
||||
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
|
||||
if (responseCode == 307 || responseCode == 308) {
|
||||
exception =
|
||||
new InvalidResponseCodeException(
|
||||
responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec);
|
||||
responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
|
||||
operation.open();
|
||||
return;
|
||||
}
|
||||
@ -665,40 +907,47 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
resetConnectTimeout();
|
||||
}
|
||||
|
||||
Map<String, List<String>> headers = info.getAllHeaders();
|
||||
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
|
||||
if (!handleSetCookieRequests) {
|
||||
request.followRedirect();
|
||||
} else {
|
||||
currentUrlRequest.cancel();
|
||||
DataSpec redirectUrlDataSpec;
|
||||
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
||||
// transformed into a GET.
|
||||
redirectUrlDataSpec =
|
||||
new DataSpec(
|
||||
Uri.parse(newLocationUrl),
|
||||
DataSpec.HTTP_METHOD_GET,
|
||||
/* httpBody= */ null,
|
||||
currentDataSpec.absoluteStreamPosition,
|
||||
currentDataSpec.position,
|
||||
currentDataSpec.length,
|
||||
currentDataSpec.key,
|
||||
currentDataSpec.flags);
|
||||
} else {
|
||||
redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
|
||||
}
|
||||
UrlRequest.Builder requestBuilder;
|
||||
try {
|
||||
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
return;
|
||||
}
|
||||
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
|
||||
attachCookies(requestBuilder, cookieHeadersValue);
|
||||
currentUrlRequest = requestBuilder.build();
|
||||
currentUrlRequest.start();
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
|
||||
if (isEmpty(setCookieHeaders)) {
|
||||
request.followRedirect();
|
||||
return;
|
||||
}
|
||||
|
||||
urlRequest.cancel();
|
||||
DataSpec redirectUrlDataSpec;
|
||||
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
||||
// transformed into a GET.
|
||||
redirectUrlDataSpec =
|
||||
new DataSpec(
|
||||
Uri.parse(newLocationUrl),
|
||||
DataSpec.HTTP_METHOD_GET,
|
||||
/* httpBody= */ null,
|
||||
dataSpec.absoluteStreamPosition,
|
||||
dataSpec.position,
|
||||
dataSpec.length,
|
||||
dataSpec.key,
|
||||
dataSpec.flags,
|
||||
dataSpec.httpRequestHeaders);
|
||||
} else {
|
||||
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
|
||||
}
|
||||
UrlRequest.Builder requestBuilder;
|
||||
try {
|
||||
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
return;
|
||||
}
|
||||
String cookieHeadersValue = parseCookies(setCookieHeaders);
|
||||
attachCookies(requestBuilder, cookieHeadersValue);
|
||||
currentUrlRequest = requestBuilder.build();
|
||||
currentUrlRequest.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,9 +20,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import java.util.concurrent.Executor;
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
||||
@ -45,8 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
|
||||
private final CronetEngineWrapper cronetEngineWrapper;
|
||||
private final Executor executor;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
private final @Nullable TransferListener transferListener;
|
||||
@Nullable private final TransferListener transferListener;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
@ -64,21 +61,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||
* suitable CronetEngine can be build.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
HttpDataSource.Factory fallbackFactory) {
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
/* transferListener= */ null,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
@ -98,20 +90,15 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
String userAgent) {
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
/* transferListener= */ null,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
@ -132,9 +119,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
@ -143,7 +127,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
@ -151,7 +134,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
/* transferListener= */ null,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
@ -172,9 +154,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
@ -184,7 +163,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
@ -192,7 +170,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
/* transferListener= */ null,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
@ -212,9 +189,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||
* suitable CronetEngine can be build.
|
||||
@ -222,11 +196,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
HttpDataSource.Factory fallbackFactory) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
false,
|
||||
fallbackFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,22 +220,27 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
String userAgent) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
||||
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
false,
|
||||
new DefaultHttpDataSourceFactory(
|
||||
userAgent,
|
||||
transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
false));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -267,9 +251,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
@ -279,16 +260,20 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
String userAgent) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
||||
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects));
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
resetTimeoutOnRedirects,
|
||||
new DefaultHttpDataSourceFactory(
|
||||
userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -299,9 +284,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
@ -312,7 +294,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
@ -320,7 +301,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
HttpDataSource.Factory fallbackFactory) {
|
||||
this.cronetEngineWrapper = cronetEngineWrapper;
|
||||
this.executor = executor;
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.transferListener = transferListener;
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
@ -339,7 +319,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
new CronetDataSource(
|
||||
cronetEngine,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
@ -37,8 +38,8 @@ public final class CronetEngineWrapper {
|
||||
|
||||
private static final String TAG = "CronetEngineWrapper";
|
||||
|
||||
private final CronetEngine cronetEngine;
|
||||
private final @CronetEngineSource int cronetEngineSource;
|
||||
@Nullable private final CronetEngine cronetEngine;
|
||||
@CronetEngineSource private final int cronetEngineSource;
|
||||
|
||||
/**
|
||||
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
|
||||
@ -144,7 +145,8 @@ public final class CronetEngineWrapper {
|
||||
*
|
||||
* @return A {@link CronetEngineSource} value.
|
||||
*/
|
||||
public @CronetEngineSource int getCronetEngineSource() {
|
||||
@CronetEngineSource
|
||||
public int getCronetEngineSource() {
|
||||
return cronetEngineSource;
|
||||
}
|
||||
|
||||
@ -153,13 +155,14 @@ public final class CronetEngineWrapper {
|
||||
*
|
||||
* @return The CronetEngine, or null if no CronetEngine is available.
|
||||
*/
|
||||
@Nullable
|
||||
/* package */ CronetEngine getCronetEngine() {
|
||||
return cronetEngine;
|
||||
}
|
||||
|
||||
private static class CronetProviderComparator implements Comparator<CronetProvider> {
|
||||
|
||||
private final String gmsCoreCronetName;
|
||||
@Nullable private final String gmsCoreCronetName;
|
||||
private final boolean preferGMSCoreCronet;
|
||||
|
||||
// Multi-catch can only be used for API 19+ in this case.
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -14,4 +14,6 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.cronet"/>
|
||||
<manifest package="com.google.android.exoplayer2.ext.cronet">
|
||||
<uses-sdk/>
|
||||
</manifest>
|
||||
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
@ -38,7 +38,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
@ -61,6 +60,7 @@ import org.chromium.net.impl.UrlResponseInfoImpl;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@ -85,7 +85,6 @@ public final class CronetDataSourceTest {
|
||||
|
||||
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
||||
@Mock private UrlRequest mockUrlRequest;
|
||||
@Mock private Predicate<String> mockContentTypePredicate;
|
||||
@Mock private TransferListener mockTransferListener;
|
||||
@Mock private Executor mockExecutor;
|
||||
@Mock private NetworkException mockNetworkException;
|
||||
@ -95,21 +94,25 @@ public final class CronetDataSourceTest {
|
||||
private boolean redirectCalled;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
HttpDataSource.RequestProperties defaultRequestProperties =
|
||||
new HttpDataSource.RequestProperties();
|
||||
defaultRequestProperties.set("defaultHeader1", "defaultValue1");
|
||||
defaultRequestProperties.set("defaultHeader2", "defaultValue2");
|
||||
|
||||
dataSourceUnderTest =
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
/* resetTimeoutOnRedirects= */ true,
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
false);
|
||||
defaultRequestProperties,
|
||||
/* handleSetCookieRequests= */ false);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||
when(mockCronetEngine.newUrlRequestBuilder(
|
||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||
.thenReturn(mockUrlRequestBuilder);
|
||||
@ -193,18 +196,59 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestHeadersSet() throws HttpDataSourceException {
|
||||
public void testRequestSetsRangeHeader() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
|
||||
dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
// The header value to add is current position to current position + length - 1.
|
||||
verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
|
||||
verify(mockUrlRequestBuilder).addHeader("firstHeader", "firstValue");
|
||||
verify(mockUrlRequestBuilder).addHeader("secondHeader", "secondValue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestHeadersSet() throws HttpDataSourceException {
|
||||
|
||||
Map<String, String> headersSet = new HashMap<>();
|
||||
doAnswer(
|
||||
(invocation) -> {
|
||||
String key = invocation.getArgument(0);
|
||||
String value = invocation.getArgument(1);
|
||||
headersSet.put(key, value);
|
||||
return null;
|
||||
})
|
||||
.when(mockUrlRequestBuilder)
|
||||
.addHeader(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());
|
||||
|
||||
dataSourceUnderTest.setRequestProperty("defaultHeader2", "dataSourceOverridesDefault");
|
||||
dataSourceUnderTest.setRequestProperty("dataSourceHeader1", "dataSourceValue1");
|
||||
dataSourceUnderTest.setRequestProperty("dataSourceHeader2", "dataSourceValue2");
|
||||
|
||||
Map<String, String> dataSpecRequestProperties = new HashMap<>();
|
||||
dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
|
||||
dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
|
||||
dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
|
||||
testDataSpec =
|
||||
new DataSpec(
|
||||
/* uri= */ Uri.parse(TEST_URL),
|
||||
/* httpMethod= */ DataSpec.HTTP_METHOD_GET,
|
||||
/* httpBody= */ null,
|
||||
/* absoluteStreamPosition= */ 1000,
|
||||
/* position= */ 1000,
|
||||
/* length= */ 5000,
|
||||
/* key= */ null,
|
||||
/* flags= */ 0,
|
||||
dataSpecRequestProperties);
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
assertThat(headersSet.get("defaultHeader1")).isEqualTo("defaultValue1");
|
||||
assertThat(headersSet.get("defaultHeader2")).isEqualTo("dataSourceOverridesDefault");
|
||||
assertThat(headersSet.get("defaultHeader3")).isEqualTo("dataSpecOverridesAll");
|
||||
assertThat(headersSet.get("dataSourceHeader1")).isEqualTo("dataSourceValue1");
|
||||
assertThat(headersSet.get("dataSourceHeader2")).isEqualTo("dataSpecOverridesDataSource");
|
||||
assertThat(headersSet.get("dataSpecHeader1")).isEqualTo("dataSpecValue1");
|
||||
|
||||
verify(mockUrlRequest).start();
|
||||
}
|
||||
|
||||
@ -245,6 +289,26 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
|
||||
testDataSpec =
|
||||
new DataSpec(
|
||||
/* uri= */ Uri.parse(TEST_URL),
|
||||
/* postBody= */ new byte[1024],
|
||||
/* absoluteStreamPosition= */ 200,
|
||||
/* position= */ 200,
|
||||
/* length= */ 1024,
|
||||
/* key= */ "key",
|
||||
/* flags= */ 0);
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail();
|
||||
} catch (IOException expected) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpenFailDueToDnsFailure() {
|
||||
mockResponseStartFailure();
|
||||
@ -283,7 +347,13 @@ public final class CronetDataSourceTest {
|
||||
@Test
|
||||
public void testRequestOpenValidatesContentTypePredicate() {
|
||||
mockResponseStartSuccess();
|
||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
|
||||
|
||||
ArrayList<String> testedContentTypes = new ArrayList<>();
|
||||
dataSourceUnderTest.setContentTypePredicate(
|
||||
(String input) -> {
|
||||
testedContentTypes.add(input);
|
||||
return false;
|
||||
});
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -292,7 +362,8 @@ public final class CronetDataSourceTest {
|
||||
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
|
||||
assertThat(testedContentTypes).hasSize(1);
|
||||
assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,6 +622,260 @@ public final class CronetDataSourceTest {
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||
|
||||
// Use a wrapped ByteBuffer instead of direct for coverage.
|
||||
returnedBuffer.rewind();
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
|
||||
// Separate cronet calls for each read.
|
||||
verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
|
||||
verify(mockTransferListener, times(2))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestIntermixRead() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
// Chunking reads into parts 6, 7, 8, 9.
|
||||
mockReadSuccess(0, 30);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
|
||||
assertThat(bytesRead).isEqualTo(6);
|
||||
|
||||
byte[] returnedBytes = new byte[7];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
|
||||
assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
|
||||
assertThat(bytesRead).isEqualTo(6 + 7);
|
||||
|
||||
returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
|
||||
assertThat(bytesRead).isEqualTo(6 + 7 + 8);
|
||||
|
||||
returnedBytes = new byte[9];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
|
||||
assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
|
||||
assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);
|
||||
|
||||
// First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
|
||||
verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
// First request.
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
dataSourceUnderTest.read(returnedBuffer);
|
||||
dataSourceUnderTest.close();
|
||||
|
||||
testResponseHeader.remove("Content-Length");
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
// Second request.
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||
returnedBuffer.limit(10);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(10);
|
||||
returnedBuffer.limit(returnedBuffer.capacity());
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(6);
|
||||
returnedBuffer.rewind();
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(1000, 5000);
|
||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||
// Tests for skipping bytes.
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 7000);
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
|
||||
testResponseHeader.remove("Content-Length");
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||
returnedBuffer.limit(8);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverreadByteBuffer() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||
|
||||
// The current buffer is kept if not completely consumed by DataSource reader.
|
||||
returnedBuffer = ByteBuffer.allocateDirect(6);
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(14);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));
|
||||
|
||||
// 2 bytes left at this point.
|
||||
returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));
|
||||
|
||||
// Called on each.
|
||||
verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
|
||||
|
||||
// Now we already returned the 16 bytes initially asked.
|
||||
// Try to read again even though all requested 16 bytes are already returned.
|
||||
// Return C.RESULT_END_OF_INPUT
|
||||
returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||
int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
|
||||
assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||
assertThat(returnedBuffer.position()).isEqualTo(0);
|
||||
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
||||
verify(mockTransferListener, never())
|
||||
.onBytesTransferred(
|
||||
dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
|
||||
// Number of calls to cronet should not have increased.
|
||||
verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
int bytesRead = 0;
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||
returnedBuffer.limit(8);
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||
returnedBuffer.flip();
|
||||
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
|
||||
dataSourceUnderTest.close();
|
||||
verify(mockTransferListener)
|
||||
.onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
|
||||
try {
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
// 16 bytes were attempted but only 8 should have been successfully read.
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectTimeout() throws InterruptedException {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
@ -734,7 +1059,6 @@ public final class CronetDataSourceTest {
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
@ -765,13 +1089,12 @@ public final class CronetDataSourceTest {
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
/* resetTimeoutOnRedirects= */ true,
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true);
|
||||
/* defaultRequestProperties= */ null,
|
||||
/* handleSetCookieRequests= */ true);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
@ -804,13 +1127,12 @@ public final class CronetDataSourceTest {
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
/* resetTimeoutOnRedirects= */ true,
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true);
|
||||
/* defaultRequestProperties= */ null,
|
||||
/* handleSetCookieRequests= */ true);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
@ -855,6 +1177,36 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferFailure() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadFailure();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
try {
|
||||
dataSourceUnderTest.read(returnedBuffer);
|
||||
fail("dataSourceUnderTest.read() returned, but IOException expected");
|
||||
} catch (IOException e) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadFailure();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
try {
|
||||
dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
|
||||
fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
mockResponseStartSuccess();
|
||||
@ -886,6 +1238,37 @@ public final class CronetDataSourceTest {
|
||||
timedOutLatch.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
final ConditionVariable startCondition = buildReadStartedCondition();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||
Thread thread =
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
dataSourceUnderTest.read(returnedBuffer);
|
||||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
||||
timedOutLatch.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
startCondition.block();
|
||||
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutLatch.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowDirectExecutor() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
@ -1064,4 +1447,17 @@ public final class CronetDataSourceTest {
|
||||
testBuffer.flip();
|
||||
return testBuffer;
|
||||
}
|
||||
|
||||
// Returns a copy of what is remaining in the src buffer from the current position to capacity.
|
||||
private static byte[] copyByteBufferToArray(ByteBuffer src) {
|
||||
if (src == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] copy = new byte[src.remaining()];
|
||||
int index = 0;
|
||||
while (src.hasRemaining()) {
|
||||
copy[index++] = src.get();
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
## Build instructions (Linux, macOS) ##
|
||||
|
||||
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
|
||||
@ -21,15 +21,15 @@ for more information).
|
||||
In addition, it's necessary to build the extension's native components as
|
||||
follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
* Set the following shell variable:
|
||||
|
||||
```
|
||||
cd "<path to exoplayer checkout>"
|
||||
FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
Only versions up to NDK 15c are supported currently.
|
||||
* Download the [Android NDK][] and set its location in a shell variable.
|
||||
This build configuration has been tested on NDK r20.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
@ -41,70 +41,21 @@ NDK_PATH="<path to Android NDK>"
|
||||
HOST_PLATFORM="linux-x86_64"
|
||||
```
|
||||
|
||||
* 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 FFmpeg release 4.0 for armeabi-v7a,
|
||||
arm64-v8a and x86 on Linux x86_64:
|
||||
* Configure the formats supported by adapting the following variable if needed
|
||||
and by setting it. See the [Supported formats][] page for more details of the
|
||||
formats.
|
||||
|
||||
```
|
||||
ENABLED_DECODERS=(vorbis opus flac)
|
||||
```
|
||||
|
||||
* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will
|
||||
fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86:
|
||||
|
||||
```
|
||||
COMMON_OPTIONS="\
|
||||
--target-os=android \
|
||||
--disable-static \
|
||||
--enable-shared \
|
||||
--disable-doc \
|
||||
--disable-programs \
|
||||
--disable-everything \
|
||||
--disable-avdevice \
|
||||
--disable-avformat \
|
||||
--disable-swscale \
|
||||
--disable-postproc \
|
||||
--disable-avfilter \
|
||||
--disable-symver \
|
||||
--disable-swresample \
|
||||
--enable-avresample \
|
||||
--enable-decoder=vorbis \
|
||||
--enable-decoder=opus \
|
||||
--enable-decoder=flac \
|
||||
" && \
|
||||
cd "${FFMPEG_EXT_PATH}" && \
|
||||
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
|
||||
cd ffmpeg && git checkout release/4.0 && \
|
||||
./configure \
|
||||
--libdir=android-libs/armeabi-v7a \
|
||||
--arch=arm \
|
||||
--cpu=armv7-a \
|
||||
--cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-" \
|
||||
--sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \
|
||||
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
|
||||
--extra-ldflags="-Wl,--fix-cortex-a8" \
|
||||
--extra-ldexeflags=-pie \
|
||||
${COMMON_OPTIONS} \
|
||||
&& \
|
||||
make -j4 && make install-libs && \
|
||||
make clean && ./configure \
|
||||
--libdir=android-libs/arm64-v8a \
|
||||
--arch=aarch64 \
|
||||
--cpu=armv8-a \
|
||||
--cross-prefix="${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-" \
|
||||
--sysroot="${NDK_PATH}/platforms/android-21/arch-arm64/" \
|
||||
--extra-ldexeflags=-pie \
|
||||
${COMMON_OPTIONS} \
|
||||
&& \
|
||||
make -j4 && make install-libs && \
|
||||
make clean && ./configure \
|
||||
--libdir=android-libs/x86 \
|
||||
--arch=x86 \
|
||||
--cpu=i686 \
|
||||
--cross-prefix="${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-" \
|
||||
--sysroot="${NDK_PATH}/platforms/android-9/arch-x86/" \
|
||||
--extra-ldexeflags=-pie \
|
||||
--disable-asm \
|
||||
${COMMON_OPTIONS} \
|
||||
&& \
|
||||
make -j4 && make install-libs && \
|
||||
make clean
|
||||
./build_ffmpeg.sh \
|
||||
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
|
||||
```
|
||||
|
||||
* Build the JNI native libraries, setting `APP_ABI` to include the architectures
|
||||
@ -115,28 +66,35 @@ cd "${FFMPEG_EXT_PATH}" && \
|
||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
|
||||
```
|
||||
|
||||
## Build instructions (Windows) ##
|
||||
|
||||
We do not provide support for building this extension on Windows, however it
|
||||
should be possible to follow the Linux instructions in [Windows PowerShell][].
|
||||
|
||||
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
|
||||
|
||||
## 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're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
|
||||
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.
|
||||
* If you're using `ExoPlayer.Builder`, 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,
|
||||
|
@ -38,9 +38,10 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -92,8 +92,9 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
@FormatSupport
|
||||
protected int supportsFormatInternal(
|
||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
||||
Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
@ -108,12 +109,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
@AdaptiveSupport
|
||||
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
|
||||
return ADAPTIVE_NOT_SEAMLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
|
@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
@ -42,7 +43,7 @@ import java.util.List;
|
||||
private static final int DECODER_ERROR_OTHER = -2;
|
||||
|
||||
private final String codecName;
|
||||
private final @Nullable byte[] extraData;
|
||||
@Nullable private final byte[] extraData;
|
||||
private final @C.Encoding int encoding;
|
||||
private final int outputBufferSize;
|
||||
|
||||
@ -106,7 +107,7 @@ import java.util.List;
|
||||
return new FfmpegDecoderException("Error resetting (see logcat).");
|
||||
}
|
||||
}
|
||||
ByteBuffer inputData = inputBuffer.data;
|
||||
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
|
||||
int inputSize = inputData.limit();
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||
@ -132,8 +133,8 @@ import java.util.List;
|
||||
}
|
||||
hasOutputFormat = true;
|
||||
}
|
||||
outputBuffer.data.position(0);
|
||||
outputBuffer.data.limit(result);
|
||||
outputData.position(0);
|
||||
outputData.limit(result);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ public final class FfmpegLibrary {
|
||||
private static final String TAG = "FfmpegLibrary";
|
||||
|
||||
private static final LibraryLoader LOADER =
|
||||
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
|
||||
new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg");
|
||||
|
||||
private FfmpegLibrary() {}
|
||||
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -22,12 +22,17 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavutil
|
||||
LOCAL_MODULE := libavresample
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavresample
|
||||
LOCAL_MODULE := libswresample
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavutil
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
@ -35,6 +40,6 @@ include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := ffmpeg
|
||||
LOCAL_SRC_FILES := ffmpeg_jni.cc
|
||||
LOCAL_C_INCLUDES := ffmpeg
|
||||
LOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil
|
||||
LOCAL_SHARED_LIBRARIES := libavcodec libavresample libswresample libavutil
|
||||
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
@ -15,6 +15,6 @@
|
||||
#
|
||||
|
||||
APP_OPTIM := release
|
||||
APP_STL := gnustl_static
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -frtti
|
||||
APP_PLATFORM := android-9
|
||||
|
85
extensions/ffmpeg/src/main/jni/build_ffmpeg.sh
Executable file
@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright (C) 2019 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.
|
||||
#
|
||||
|
||||
FFMPEG_EXT_PATH=$1
|
||||
NDK_PATH=$2
|
||||
HOST_PLATFORM=$3
|
||||
ENABLED_DECODERS=("${@:4}")
|
||||
COMMON_OPTIONS="
|
||||
--target-os=android
|
||||
--disable-static
|
||||
--enable-shared
|
||||
--disable-doc
|
||||
--disable-programs
|
||||
--disable-everything
|
||||
--disable-avdevice
|
||||
--disable-avformat
|
||||
--disable-swscale
|
||||
--disable-postproc
|
||||
--disable-avfilter
|
||||
--disable-symver
|
||||
--enable-avresample
|
||||
--enable-swresample
|
||||
"
|
||||
TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
|
||||
for decoder in "${ENABLED_DECODERS[@]}"
|
||||
do
|
||||
COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
|
||||
done
|
||||
cd "${FFMPEG_EXT_PATH}"
|
||||
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
|
||||
cd ffmpeg
|
||||
git checkout release/4.2
|
||||
./configure \
|
||||
--libdir=android-libs/armeabi-v7a \
|
||||
--arch=arm \
|
||||
--cpu=armv7-a \
|
||||
--cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \
|
||||
--nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \
|
||||
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
|
||||
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
|
||||
--extra-ldflags="-Wl,--fix-cortex-a8" \
|
||||
--extra-ldexeflags=-pie \
|
||||
${COMMON_OPTIONS}
|
||||
make -j4
|
||||
make install-libs
|
||||
make clean
|
||||
./configure \
|
||||
--libdir=android-libs/arm64-v8a \
|
||||
--arch=aarch64 \
|
||||
--cpu=armv8-a \
|
||||
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
|
||||
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
|
||||
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
|
||||
--extra-ldexeflags=-pie \
|
||||
${COMMON_OPTIONS}
|
||||
make -j4
|
||||
make install-libs
|
||||
make clean
|
||||
./configure \
|
||||
--libdir=android-libs/x86 \
|
||||
--arch=x86 \
|
||||
--cpu=i686 \
|
||||
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
|
||||
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
|
||||
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
|
||||
--extra-ldexeflags=-pie \
|
||||
--disable-asm \
|
||||
${COMMON_OPTIONS}
|
||||
make -j4
|
||||
make install-libs
|
||||
make clean
|
@ -14,4 +14,6 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.ffmpeg"/>
|
||||
<manifest package="com.google.android.exoplayer2.ext.ffmpeg">
|
||||
<uses-sdk/>
|
||||
</manifest>
|
||||
|
@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
## Build instructions (Linux, macOS) ##
|
||||
|
||||
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
|
||||
@ -28,8 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
|
||||
FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] (version <= 17c) and set its location in an
|
||||
environment variable:
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
This build configuration has been tested on NDK r20.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
@ -53,6 +53,13 @@ ${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
|
||||
|
||||
## Build instructions (Windows) ##
|
||||
|
||||
We do not provide support for building this extension on Windows, however it
|
||||
should be possible to follow the Linux instructions in [Windows PowerShell][].
|
||||
|
||||
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Once you've followed the instructions above to check out, build and depend on
|
||||
@ -68,22 +75,22 @@ renderer.
|
||||
|
||||
### 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're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
|
||||
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.
|
||||
* If you're using `ExoPlayer.Builder`, 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,
|
||||
|
@ -39,11 +39,14 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link FlacBinarySearchSeeker}. */
|
||||
@ -41,6 +42,7 @@ public final class FlacBinarySearchSeekerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
|
||||
throws IOException, FlacDecoderException, InterruptedException {
|
||||
byte[] data =
|
||||
@ -63,6 +65,7 @@ public final class FlacBinarySearchSeekerTest {
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetSeekTargetUs_returnsSeekPending()
|
||||
throws IOException, FlacDecoderException, InterruptedException {
|
||||
byte[] data =
|
||||
|
@ -41,6 +41,7 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
|
||||
@ -76,6 +77,7 @@ public final class FlacExtractorSeekTest {
|
||||
positionHolder = new PositionHolder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
@ -87,6 +89,7 @@ public final class FlacExtractorSeekTest {
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
@ -103,6 +106,7 @@ public final class FlacExtractorSeekTest {
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
@ -120,6 +124,7 @@ public final class FlacExtractorSeekTest {
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
@ -139,6 +144,7 @@ public final class FlacExtractorSeekTest {
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
@ -158,6 +164,7 @@ public final class FlacExtractorSeekTest {
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
@ -228,7 +235,8 @@ public final class FlacExtractorSeekTest {
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
|
||||
@Nullable
|
||||
private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
|
||||
throws IOException, InterruptedException {
|
||||
try {
|
||||
ExtractorInput input = getExtractorInputFromPosition(0);
|
||||
|
@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link FlacExtractor}. */
|
||||
@ -34,11 +35,13 @@ public class FlacExtractorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractFlacSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractFlacSampleWithId3Header() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
|
||||
|
@ -24,13 +24,10 @@ import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@ -82,8 +79,7 @@ public class FlacPlaybackTest {
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
||||
player = new ExoPlayer.Builder(context, audioRenderer).build();
|
||||
player.addListener(this);
|
||||
MediaSource mediaSource =
|
||||
new ProgressiveMediaSource.Factory(
|
||||
@ -101,7 +97,7 @@ public class FlacPlaybackTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED
|
||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||
player.release();
|
||||
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
@ -101,7 +102,7 @@ import java.util.List;
|
||||
if (reset) {
|
||||
decoderJni.flush();
|
||||
}
|
||||
decoderJni.setData(inputBuffer.data);
|
||||
decoderJni.setData(Util.castNonNull(inputBuffer.data));
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
|
||||
try {
|
||||
decoderJni.decodeSample(outputData);
|
||||
|
@ -51,6 +51,12 @@ import java.nio.ByteBuffer;
|
||||
@Nullable private byte[] tempBuffer;
|
||||
private boolean endOfExtractorInput;
|
||||
|
||||
// the constructor does not initialize fields: tempBuffer
|
||||
// call to flacInit() not allowed on the given receiver.
|
||||
@SuppressWarnings({
|
||||
"nullness:initialization.fields.uninitialized",
|
||||
"nullness:method.invocation.invalid"
|
||||
})
|
||||
public FlacDecoderJni() throws FlacDecoderException {
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
throw new FlacDecoderException("Failed to load decoder native libraries.");
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
private static final int NUM_BUFFERS = 16;
|
||||
|
||||
public LibflacAudioRenderer() {
|
||||
this(null, null);
|
||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,15 +44,16 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public LibflacAudioRenderer(
|
||||
Handler eventHandler,
|
||||
AudioRendererEventListener eventListener,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, audioProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
@FormatSupport
|
||||
protected int supportsFormatInternal(
|
||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
||||
if (!FlacLibrary.isAvailable()
|
||||
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
@ -65,7 +67,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws FlacDecoderException {
|
||||
return new FlacDecoder(
|
||||
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -15,6 +15,6 @@
|
||||
#
|
||||
|
||||
APP_OPTIM := release
|
||||
APP_STL := gnustl_static
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -frtti
|
||||
APP_PLATFORM := android-14
|
||||
|
@ -265,11 +265,11 @@ FLACParser::FLACParser(DataSource *source)
|
||||
: mDataSource(source),
|
||||
mCopy(copyTrespass),
|
||||
mDecoder(NULL),
|
||||
mSeekTable(NULL),
|
||||
firstFrameOffset(0LL),
|
||||
mCurrentPos(0LL),
|
||||
mEOF(false),
|
||||
mStreamInfoValid(false),
|
||||
mSeekTable(NULL),
|
||||
firstFrameOffset(0LL),
|
||||
mVorbisCommentsValid(false),
|
||||
mPicturesValid(false),
|
||||
mWriteRequested(false),
|
||||
@ -456,6 +456,9 @@ bool FLACParser::getSeekPositions(int64_t timeUs,
|
||||
|
||||
for (unsigned i = length; i != 0; i--) {
|
||||
int64_t sampleNumber = points[i - 1].sample_number;
|
||||
if (sampleNumber == -1) { // placeholder
|
||||
continue;
|
||||
}
|
||||
if (sampleNumber <= targetSampleNumber) {
|
||||
result[0] = (sampleNumber * 1000000LL) / sampleRate;
|
||||
result[1] = firstFrameOffset + points[i - 1].stream_offset;
|
||||
|
@ -62,7 +62,9 @@ class FLACParser {
|
||||
|
||||
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
|
||||
|
||||
std::vector<std::string> getVorbisComments() { return mVorbisComments; }
|
||||
const std::vector<std::string>& getVorbisComments() const {
|
||||
return mVorbisComments;
|
||||
}
|
||||
|
||||
bool arePicturesValid() const { return mPicturesValid; }
|
||||
|
||||
|
@ -14,4 +14,6 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.flac"/>
|
||||
<manifest package="com.google.android.exoplayer2.ext.flac">
|
||||
<uses-sdk/>
|
||||
</manifest>
|
||||
|
@ -1,11 +1,15 @@
|
||||
# ExoPlayer GVR extension #
|
||||
|
||||
**DEPRECATED - If you still need this extension, please contact us by filing an
|
||||
issue on our [issue tracker][].**
|
||||
|
||||
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.
|
||||
|
||||
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
||||
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
||||
[issue tracker]: https://github.com/google/ExoPlayer/issues
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
|
@ -33,7 +33,7 @@ android {
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
api 'com.google.vr:sdk-base:1.190.0'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.gvr;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.vr.sdk.audio.GvrAudioSurround;
|
||||
@ -28,7 +27,11 @@ import java.nio.ByteOrder;
|
||||
/**
|
||||
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
|
||||
* surround sound and ambisonic soundfields.
|
||||
*
|
||||
* @deprecated If you still need this component, please contact us by filing an issue on our <a
|
||||
* href="https://github.com/google/ExoPlayer/issues">issue tracker</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class GvrAudioProcessor implements AudioProcessor {
|
||||
|
||||
static {
|
||||
@ -40,8 +43,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
|
||||
private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;
|
||||
|
||||
private int sampleRateHz;
|
||||
private int channelCount;
|
||||
private AudioFormat pendingInputAudioFormat;
|
||||
private int pendingGvrAudioSurroundFormat;
|
||||
@Nullable private GvrAudioSurround gvrAudioSurround;
|
||||
private ByteBuffer buffer;
|
||||
@ -56,8 +58,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
public GvrAudioProcessor() {
|
||||
// Use the identity for the initial orientation.
|
||||
w = 1f;
|
||||
sampleRateHz = Format.NO_VALUE;
|
||||
channelCount = Format.NO_VALUE;
|
||||
pendingInputAudioFormat = AudioFormat.NOT_SET;
|
||||
buffer = EMPTY_BUFFER;
|
||||
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
|
||||
}
|
||||
@ -83,19 +84,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@Override
|
||||
public synchronized boolean configure(
|
||||
int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||
throws UnhandledFormatException {
|
||||
if (encoding != C.ENCODING_PCM_16BIT) {
|
||||
public synchronized AudioFormat configure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
throw new UnhandledAudioFormatException(inputAudioFormat);
|
||||
}
|
||||
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
|
||||
return false;
|
||||
}
|
||||
this.sampleRateHz = sampleRateHz;
|
||||
this.channelCount = channelCount;
|
||||
switch (channelCount) {
|
||||
switch (inputAudioFormat.channelCount) {
|
||||
case 1:
|
||||
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
|
||||
break;
|
||||
@ -115,13 +110,14 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
|
||||
break;
|
||||
default:
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
throw new UnhandledAudioFormatException(inputAudioFormat);
|
||||
}
|
||||
if (buffer == EMPTY_BUFFER) {
|
||||
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
||||
.order(ByteOrder.nativeOrder());
|
||||
}
|
||||
return true;
|
||||
pendingInputAudioFormat = inputAudioFormat;
|
||||
return new AudioFormat(inputAudioFormat.sampleRate, OUTPUT_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -129,21 +125,6 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputChannelCount() {
|
||||
return OUTPUT_CHANNEL_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputEncoding() {
|
||||
return C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputSampleRateHz() {
|
||||
return sampleRateHz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer input) {
|
||||
int position = input.position();
|
||||
@ -182,7 +163,10 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
gvrAudioSurround =
|
||||
new GvrAudioSurround(
|
||||
pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER);
|
||||
pendingGvrAudioSurroundFormat,
|
||||
pendingInputAudioFormat.sampleRate,
|
||||
pendingInputAudioFormat.channelCount,
|
||||
FRAMES_PER_OUTPUT_BUFFER);
|
||||
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
||||
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
|
||||
} else if (gvrAudioSurround != null) {
|
||||
@ -196,8 +180,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
|
||||
inputEnded = false;
|
||||
sampleRateHz = Format.NO_VALUE;
|
||||
channelCount = Format.NO_VALUE;
|
||||
pendingInputAudioFormat = AudioFormat.NOT_SET;
|
||||
buffer = EMPTY_BUFFER;
|
||||
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
|
||||
}
|
||||
|
@ -1,355 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.gvr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.Matrix;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.BinderThread;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.spherical.GlViewGroup;
|
||||
import com.google.android.exoplayer2.ui.spherical.PointerRenderer;
|
||||
import com.google.android.exoplayer2.ui.spherical.SceneRenderer;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.vr.ndk.base.DaydreamApi;
|
||||
import com.google.vr.sdk.base.AndroidCompat;
|
||||
import com.google.vr.sdk.base.Eye;
|
||||
import com.google.vr.sdk.base.GvrActivity;
|
||||
import com.google.vr.sdk.base.GvrView;
|
||||
import com.google.vr.sdk.base.HeadTransform;
|
||||
import com.google.vr.sdk.base.Viewport;
|
||||
import com.google.vr.sdk.controller.Controller;
|
||||
import com.google.vr.sdk.controller.ControllerManager;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Base activity for VR 360 video playback. */
|
||||
public abstract class GvrPlayerActivity extends GvrActivity {
|
||||
|
||||
private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
|
||||
|
||||
private final Handler mainHandler;
|
||||
|
||||
@Nullable private Player player;
|
||||
@MonotonicNonNull private GlViewGroup glView;
|
||||
@MonotonicNonNull private ControllerManager controllerManager;
|
||||
@MonotonicNonNull private SurfaceTexture surfaceTexture;
|
||||
@MonotonicNonNull private Surface surface;
|
||||
@MonotonicNonNull private SceneRenderer scene;
|
||||
@MonotonicNonNull private PlayerControlView playerControl;
|
||||
|
||||
public GvrPlayerActivity() {
|
||||
mainHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setScreenAlwaysOn(true);
|
||||
|
||||
GvrView gvrView = new GvrView(this);
|
||||
// Since videos typically have fewer pixels per degree than the phones, reducing the render
|
||||
// target scaling factor reduces the work required to render the scene.
|
||||
gvrView.setRenderTargetScale(.5f);
|
||||
|
||||
// If a custom theme isn't specified, the Context's theme is used. For VR Activities, this is
|
||||
// the old Android default theme rather than a modern theme. Override this with a custom theme.
|
||||
Context theme = new ContextThemeWrapper(this, R.style.VrTheme);
|
||||
glView = new GlViewGroup(theme, R.layout.vr_ui);
|
||||
|
||||
playerControl = Assertions.checkNotNull(glView.findViewById(R.id.controller));
|
||||
playerControl.setShowVrButton(true);
|
||||
playerControl.setVrButtonListener(v -> exit());
|
||||
|
||||
PointerRenderer pointerRenderer = new PointerRenderer();
|
||||
scene = new SceneRenderer();
|
||||
Renderer renderer = new Renderer(scene, glView, pointerRenderer);
|
||||
|
||||
// Attach glView to gvrView in order to properly handle UI events.
|
||||
gvrView.addView(glView, 0);
|
||||
|
||||
// Standard GvrView configuration
|
||||
gvrView.setEGLConfigChooser(
|
||||
8, 8, 8, 8, // RGBA bits.
|
||||
16, // Depth bits.
|
||||
0); // Stencil bits.
|
||||
gvrView.setRenderer(renderer);
|
||||
setContentView(gvrView);
|
||||
|
||||
// Most Daydream phones can render a 4k video at 60fps in sustained performance mode. These
|
||||
// options can be tweaked along with the render target scale.
|
||||
if (gvrView.setAsyncReprojectionEnabled(true)) {
|
||||
AndroidCompat.setSustainedPerformanceMode(this, true);
|
||||
}
|
||||
|
||||
// Handle the user clicking on the 'X' in the top left corner. Since this is done when the user
|
||||
// has taken the headset out of VR, it should launch the app's exit flow directly rather than
|
||||
// using the transition flow.
|
||||
gvrView.setOnCloseButtonListener(this::finish);
|
||||
|
||||
ControllerManager.EventListener listener =
|
||||
new ControllerManager.EventListener() {
|
||||
@Override
|
||||
public void onApiStatusChanged(int status) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecentered() {
|
||||
// TODO if in cardboard mode call gvrView.recenterHeadTracker();
|
||||
glView.post(() -> Util.castNonNull(playerControl).show());
|
||||
}
|
||||
};
|
||||
controllerManager = new ControllerManager(this, listener);
|
||||
|
||||
Controller controller = controllerManager.getController();
|
||||
ControllerEventListener controllerEventListener =
|
||||
new ControllerEventListener(controller, pointerRenderer, glView);
|
||||
controller.setEventListener(controllerEventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Player} to use.
|
||||
*
|
||||
* @param newPlayer The {@link Player} to use, or {@code null} to detach the current player.
|
||||
*/
|
||||
protected void setPlayer(@Nullable Player newPlayer) {
|
||||
Assertions.checkNotNull(scene);
|
||||
if (player == newPlayer) {
|
||||
return;
|
||||
}
|
||||
if (player != null) {
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
if (surface != null) {
|
||||
videoComponent.clearVideoSurface(surface);
|
||||
}
|
||||
videoComponent.clearVideoFrameMetadataListener(scene);
|
||||
videoComponent.clearCameraMotionListener(scene);
|
||||
}
|
||||
}
|
||||
player = newPlayer;
|
||||
if (player != null) {
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoFrameMetadataListener(scene);
|
||||
videoComponent.setCameraMotionListener(scene);
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
}
|
||||
Assertions.checkNotNull(playerControl).setPlayer(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one
|
||||
* is used.
|
||||
*
|
||||
* @param stereoMode A {@link C.StereoMode} value.
|
||||
*/
|
||||
protected void setDefaultStereoMode(@C.StereoMode int stereoMode) {
|
||||
Assertions.checkNotNull(scene).setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent unused) {
|
||||
if (requestCode == EXIT_FROM_VR_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
Util.castNonNull(controllerManager).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Util.castNonNull(controllerManager).stop();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
setPlayer(null);
|
||||
releaseSurface(surfaceTexture, surface);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/** Tries to exit gracefully from VR using a VR transition dialog. */
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
protected void exit() {
|
||||
// This needs to use GVR's exit transition to avoid disorienting the user.
|
||||
DaydreamApi api = DaydreamApi.create(this);
|
||||
if (api != null) {
|
||||
api.exitFromVr(this, EXIT_FROM_VR_REQUEST_CODE, null);
|
||||
// Eventually, the Activity's onActivityResult will be called.
|
||||
api.close();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles PlayerControl visibility. */
|
||||
@UiThread
|
||||
protected void togglePlayerControlVisibility() {
|
||||
if (Assertions.checkNotNull(playerControl).isVisible()) {
|
||||
playerControl.hide();
|
||||
} else {
|
||||
playerControl.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Called on GL thread.
|
||||
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
|
||||
Surface oldSurface = this.surface;
|
||||
this.surfaceTexture = surfaceTexture;
|
||||
this.surface = new Surface(surfaceTexture);
|
||||
if (player != null) {
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
}
|
||||
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||
});
|
||||
}
|
||||
|
||||
private static void releaseSurface(
|
||||
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
|
||||
if (oldSurfaceTexture != null) {
|
||||
oldSurfaceTexture.release();
|
||||
}
|
||||
if (oldSurface != null) {
|
||||
oldSurface.release();
|
||||
}
|
||||
}
|
||||
|
||||
private class Renderer implements GvrView.StereoRenderer {
|
||||
private static final float Z_NEAR = .1f;
|
||||
private static final float Z_FAR = 100;
|
||||
|
||||
private final float[] viewProjectionMatrix = new float[16];
|
||||
private final SceneRenderer scene;
|
||||
private final GlViewGroup glView;
|
||||
private final PointerRenderer pointerRenderer;
|
||||
|
||||
public Renderer(SceneRenderer scene, GlViewGroup glView, PointerRenderer pointerRenderer) {
|
||||
this.scene = scene;
|
||||
this.glView = glView;
|
||||
this.pointerRenderer = pointerRenderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewFrame(HeadTransform headTransform) {}
|
||||
|
||||
@Override
|
||||
public void onDrawEye(Eye eye) {
|
||||
Matrix.multiplyMM(
|
||||
viewProjectionMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, eye.getEyeView(), 0);
|
||||
scene.drawFrame(viewProjectionMatrix, eye.getType() == Eye.Type.RIGHT);
|
||||
if (glView.isVisible()) {
|
||||
glView.getRenderer().draw(viewProjectionMatrix);
|
||||
pointerRenderer.draw(viewProjectionMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishFrame(Viewport viewport) {}
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(EGLConfig config) {
|
||||
onSurfaceTextureAvailable(scene.init());
|
||||
glView.getRenderer().init();
|
||||
pointerRenderer.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(int width, int height) {}
|
||||
|
||||
@Override
|
||||
public void onRendererShutdown() {
|
||||
glView.getRenderer().shutdown();
|
||||
pointerRenderer.shutdown();
|
||||
scene.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private class ControllerEventListener extends Controller.EventListener {
|
||||
|
||||
private final Controller controller;
|
||||
private final PointerRenderer pointerRenderer;
|
||||
private final GlViewGroup glView;
|
||||
private final float[] controllerOrientationMatrix;
|
||||
private boolean clickButtonDown;
|
||||
private boolean appButtonDown;
|
||||
|
||||
public ControllerEventListener(
|
||||
Controller controller, PointerRenderer pointerRenderer, GlViewGroup glView) {
|
||||
this.controller = controller;
|
||||
this.pointerRenderer = pointerRenderer;
|
||||
this.glView = glView;
|
||||
controllerOrientationMatrix = new float[16];
|
||||
}
|
||||
|
||||
@Override
|
||||
@BinderThread
|
||||
public void onUpdate() {
|
||||
controller.update();
|
||||
controller.orientation.toRotationMatrix(controllerOrientationMatrix);
|
||||
pointerRenderer.setControllerOrientation(controllerOrientationMatrix);
|
||||
|
||||
if (clickButtonDown || controller.clickButtonState) {
|
||||
int action;
|
||||
if (clickButtonDown != controller.clickButtonState) {
|
||||
clickButtonDown = controller.clickButtonState;
|
||||
action = clickButtonDown ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;
|
||||
} else {
|
||||
action = MotionEvent.ACTION_MOVE;
|
||||
}
|
||||
glView.post(
|
||||
() -> {
|
||||
float[] angles = controller.orientation.toYawPitchRollRadians(new float[3]);
|
||||
boolean clickedOnView = glView.simulateClick(action, angles[0], angles[1]);
|
||||
if (action == MotionEvent.ACTION_DOWN && !clickedOnView) {
|
||||
togglePlayerControlVisibility();
|
||||
}
|
||||
});
|
||||
} else if (!appButtonDown && controller.appButtonState) {
|
||||
glView.post(GvrPlayerActivity.this::togglePlayerControlVisibility);
|
||||
}
|
||||
appButtonDown = controller.appButtonState;
|
||||
}
|
||||
}
|
||||
}
|