Merge pull request #1 from google/dev-v2

Dev v2 - 1
This commit is contained in:
sr90 2019-08-12 13:38:39 -07:00 committed by GitHub
commit eb458555a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
517 changed files with 15010 additions and 7120 deletions

View file

@ -8,9 +8,12 @@ assignees: ''
Before filing a bug:
-----------------------
- Search existing issues, including issues that are closed.
- Consult our FAQs, supported devices and supported formats pages. These can be
found at https://exoplayer.dev/.
- Search existing issues, including issues that are closed:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our developer website, which can be found at https://exoplayer.dev/.
It provides detailed information about supported formats and devices.
- Learn how to create useful log output by using the EventLogger:
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here:
@ -33,16 +36,17 @@ or a small sample app that youre able to share as source code on GitHub.
Provide a JSON snippet for the demo apps media.exolist.json file, or a link to
media that reproduces the issue. If you don't wish to post it publicly, please
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
in the format "Issue #1234". Provide all the metadata we'd need to play the
content like drm license urls or similar. If the content is accessible only in
certain countries or regions, please say so.
in the format "Issue #1234", where "#1234" should be replaced with your issue
number. Provide all the metadata we'd need to play the content like drm license
urls or similar. If the content is accessible only in certain countries or
regions, please say so.
### [REQUIRED] A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234".
"Issue #1234", where "#1234" should be replaced with your issue number.
### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".

View file

@ -8,9 +8,12 @@ assignees: ''
Before filing a content issue:
------------------------------
- Search existing issues, including issues that are closed.
- Search existing issues, including issues that are closed:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our supported formats page, which can be found at
https://exoplayer.dev/supported-formats.html.
- Learn how to create useful log output by using the EventLogger:
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Try playing your content in the ExoPlayer demo app. Information about the
ExoPlayer demo app can be found here:
http://exoplayer.dev/demo-application.html.
@ -30,9 +33,10 @@ and you expect to play, like 5.1 audio track, text tracks or drm systems.
Provide a JSON snippet for the demo apps media.exolist.json file, or a link to
media that reproduces the issue. If you don't wish to post it publicly, please
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
in the format "Issue #1234". Provide all the metadata we'd need to play the
content like drm license urls or similar. If the content is accessible only in
certain countries or regions, please say so.
in the format "Issue #1234", where "#1234" should be replaced with your issue
number. Provide all the metadata we'd need to play the content like drm license
urls or similar. If the content is accessible only in certain countries or
regions, please say so.
### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
@ -41,6 +45,13 @@ Specify the absolute version number. Avoid using terms such as "latest".
Specify the devices and versions of Android on which you expect the content to
play. If possible, please test on multiple devices and Android versions.
### [REQUIRED] A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234", where "#1234" should be replaced with your issue number.
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/content_not_playing.md

View file

@ -8,8 +8,9 @@ assignees: ''
Before filing a feature request:
-----------------------
- Search existing open issues, specifically with the label enhancement.
- Search existing pull requests.
- Search existing open issues, specifically with the label enhancement:
https://github.com/google/ExoPlayer/labels/enhancement
- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
When filing a feature request:
-----------------------

View file

@ -12,8 +12,12 @@ Before filing a question:
a general Android development question, please do so on Stack Overflow.
- Search existing issues, including issues that are closed. Its often the
quickest way to get an answer!
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These
can be found at https://exoplayer.dev/.
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our developer website, which can be found at https://exoplayer.dev/.
It provides detailed information about supported formats, devices as well as
information about how to use the ExoPlayer library.
- The ExoPlayer library Javadoc can be found at
https://exoplayer.dev/doc/reference/
When filing a question:
-----------------------
@ -28,6 +32,23 @@ important for us to know this so that we can improve our documentation.
### [REQUIRED] Question
Describe your question in detail.
### A full bug report captured from the device
In case your question refers to a problem you are seeing in your app, capture a
full bug report using "adb bugreport". Please attach the captured bug report as
a file. If you don't wish to post it publicly, please submit the issue, then
email the bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234", where "#1234" should be replaced with your issue number.
### Link to test content
In case your question is related to a piece of media, which you are trying to
play, please provide a JSON snippet for the demo apps media.exolist.json file,
or a link to media that reproduces the issue. If you don't wish to post it
publicly, please submit the issue, then email the link to
dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where
"#1234" should be replaced with your issue number. Provide all the metadata we'd
need to play the content like drm license urls or similar. If the content is
accessible only in certain countries or regions, please say so.
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/question.md

View file

@ -2,18 +2,135 @@
### dev-v2 (not yet released) ###
* Update `DefaultTrackSelector` to apply a viewport constraint for the default
display by default.
* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis
and analytics reporting (TODO: link to developer guide page/blog post).
* Add basic DRM support to the Cast demo app.
* Assume that encrypted content requires secure decoders in renderer support
checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)).
* Decoders: Prefer decoders that advertise format support over ones that do not,
even if they are listed lower in the `MediaCodecList`.
* CEA-608: Handle XDS and TEXT modes
([5807](https://github.com/google/ExoPlayer/pull/5807)).
* Audio: fix an issue where not all audio was played out when the configuration
for the underlying track was changing (e.g., at some period transitions).
* UI: Change playback controls toggle from touch down to touch up events
([#5784](https://github.com/google/ExoPlayer/issues/5784)).
* Add a workaround for broken raw audio decoding on Oppo R9
([#5782](https://github.com/google/ExoPlayer/issues/5782)).
* Add VR player demo.
* Wrap decoder exceptions in a new `DecoderException` class and report as
renderer error.
* Do not pass the manifest to callbacks of `Player.EventListener` and
`SourceInfoRefreshListener` anymore. Instead make it accessible through
`Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename
`SourceInfoRefreshListener` to `MediaSourceCaller`.
* Set `compileSdkVersion` to 29 to use Android Q APIs.
* Add `enable` and `disable` methods to `MediaSource` to improve resource
management in playlists.
* Improve text selection logic to always prefer the better language matches
over other selection parameters.
* Remove `AnalyticsCollector.Factory`. Instances can be created directly and
the `Player` set later using `AnalyticsCollector.setPlayer`.
* Add `allowAudioMixedChannelCountAdaptiveness` parameter to
`DefaultTrackSelector` to allow adaptive selections of audio tracks with
different channel counts
([#6257](https://github.com/google/ExoPlayer/issues/6257)).
### 2.10.4 ###
* Offline: Add `Scheduler` implementation that uses `WorkManager`.
* Add ability to specify a description when creating notification channels via
ExoPlayer library classes.
* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language
tags instead of 3-letter ISO 639-2 language tags.
* Ensure the `SilenceMediaSource` position is in range
([#6229](https://github.com/google/ExoPlayer/issues/6229)).
* WAV: Calculate correct duration for clipped streams
([#6241](https://github.com/google/ExoPlayer/issues/6241)).
* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).
* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
* Fix issue where initial seek positions get ignored when playing a preroll ad
([#6201](https://github.com/google/ExoPlayer/issues/6201)).
* Fix issue where invalid language tags were normalized to "und" instead of
keeping the original
([#6153](https://github.com/google/ExoPlayer/issues/6153)).
* Fix `DataSchemeDataSource` re-opening and range requests
([#6192](https://github.com/google/ExoPlayer/issues/6192)).
* Fix Flac and ALAC playback on some LG devices
([#5938](https://github.com/google/ExoPlayer/issues/5938)).
* Fix issue when calling `performClick` on `PlayerView` without
`PlayerControlView`
([#6260](https://github.com/google/ExoPlayer/issues/6260)).
* Fix issue where playback speeds are not used in adaptive track selections
after manual selection changes for other renderers
([#6256](https://github.com/google/ExoPlayer/issues/6256)).
### 2.10.3 ###
* Display last frame when seeking to end of stream
([#2568](https://github.com/google/ExoPlayer/issues/2568)).
* Audio:
* Fix an issue where not all audio was played out when the configuration
for the underlying track was changing (e.g., at some period transitions).
* Fix an issue where playback speed was applied inaccurately in playlists
([#6117](https://github.com/google/ExoPlayer/issues/6117)).
* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is
attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)).
* CEA608: Fix repetition of special North American characters
([#6133](https://github.com/google/ExoPlayer/issues/6133)).
* FLV: Fix bug that caused playback of some live streams to not start
([#6111](https://github.com/google/ExoPlayer/issues/6111)).
* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`.
* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming
playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)).
### 2.10.2 ###
* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s
([#5779](https://github.com/google/ExoPlayer/issues/5779)).
* Add `SilenceMediaSource` that can be used to play silence of a given
duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)).
* Offline:
* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after
preparation of a `DownloadHelper` fails
([#5915](https://github.com/google/ExoPlayer/issues/5915)).
* Fix `CacheUtil.cache()` downloading too much data
([#5927](https://github.com/google/ExoPlayer/issues/5927)).
* Fix misreporting cached bytes when caching is paused
([#5573](https://github.com/google/ExoPlayer/issues/5573)).
* UI:
* Allow setting `DefaultTimeBar` attributes on `PlayerView` and
`PlayerControlView`.
* Change playback controls toggle from touch down to touch up events
([#5784](https://github.com/google/ExoPlayer/issues/5784)).
* Fix issue where playback controls were not kept visible on key presses
([#5963](https://github.com/google/ExoPlayer/issues/5963)).
* Subtitles:
* CEA-608: Handle XDS and TEXT modes
([#5807](https://github.com/google/ExoPlayer/pull/5807)).
* TTML: Fix bitmap rendering
([#5633](https://github.com/google/ExoPlayer/pull/5633)).
* IMA: Fix ad pod index offset calculation without preroll
([#5928](https://github.com/google/ExoPlayer/issues/5928)).
* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods
to indicate whether a controller sent a play or only a prepare command. This
allows to take advantage of decoder reuse with the MediaSessionConnector
([#5891](https://github.com/google/ExoPlayer/issues/5891)).
* Add `ProgressUpdateListener` to `PlayerControlView`
([#5834](https://github.com/google/ExoPlayer/issues/5834)).
* Add support for auto-detecting UDP streams in `DefaultDataSource`
([#6036](https://github.com/google/ExoPlayer/pull/6036)).
* Allow enabling decoder fallback with `DefaultRenderersFactory`
([#5942](https://github.com/google/ExoPlayer/issues/5942)).
* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission
([#6019](https://github.com/google/ExoPlayer/issues/6019)).
* Fix decoding problems when seeking back after seeking beyond a mid-roll ad
([#6009](https://github.com/google/ExoPlayer/issues/6009)).
* Fix application of `maxAudioBitrate` for adaptive audio track groups
([#6006](https://github.com/google/ExoPlayer/issues/6006)).
* Fix bug caused by parallel adaptive track selection using `Format`s without
bitrate information
([#5971](https://github.com/google/ExoPlayer/issues/5971)).
* Fix bug in `CastPlayer.getCurrentWindowIndex()`
([#5955](https://github.com/google/ExoPlayer/issues/5955)).
### 2.10.1 ###

View file

@ -21,14 +21,6 @@ buildscript {
classpath 'com.novoda:bintray-release:0.9'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
}
}
allprojects {
repositories {
@ -44,6 +36,7 @@ allprojects {
}
buildDir = "${externalBuildDir}/${project.name}"
}
group = 'com.google.android.exoplayer'
}
apply from: 'javadoc_combined.gradle'

View file

@ -13,17 +13,18 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.10.1'
releaseVersionCode = 2010001
releaseVersion = '2.10.4'
releaseVersionCode = 2010004
minSdkVersion = 16
targetSdkVersion = 28
compileSdkVersion = 28
compileSdkVersion = 29
dexmakerVersion = '2.21.0'
mockitoVersion = '2.25.0'
robolectricVersion = '4.3-alpha-2'
robolectricVersion = '4.3'
autoValueVersion = '1.6'
autoServiceVersion = '1.0-rc4'
checkerframeworkVersion = '2.5.0'
jsr305Version = '3.0.2'
androidXTestVersion = '1.1.0'
truthVersion = '0.44'
modulePrefix = ':'

View file

@ -24,7 +24,6 @@ include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui'
include modulePrefix + 'testutils'
include modulePrefix + 'testutils-robolectric'
include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
@ -38,6 +37,7 @@ include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
@ -46,7 +46,6 @@ 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-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')
@ -60,3 +59,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')

View file

@ -47,17 +47,6 @@ android {
// The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
}
dependencies {

View file

@ -25,7 +25,7 @@
android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="${castOptionsProvider}" />
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View file

@ -1,405 +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 boolean castMediaQueueCreationPending;
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, @Player.State int playbackState) {
updateCurrentItemIndex();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (currentPlayer == castPlayer && timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
}
// 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.
castMediaQueueCreationPending = currentPlayer == castPlayer;
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 (castMediaQueueCreationPending) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = buildMediaQueueItem(mediaQueue.get(i));
}
castMediaQueueCreationPending = false;
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();
}
}

View file

@ -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);
}

View file

@ -17,6 +17,8 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@ -39,11 +41,9 @@ 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;
}
}
}

View file

@ -15,12 +15,51 @@
*/
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.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.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.FrameworkMediaCrypto;
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.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.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
import java.util.IdentityHashMap;
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 +67,418 @@ 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;
private final IdentityHashMap<MediaSource, FrameworkMediaDrm> mediaDrms;
/** 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();
mediaDrms = new IdentityHashMap<>();
/** Returns the {@link MediaItem} at the given {@code position}. */
MediaItem getItem(int position);
trackSelector = new DefaultTrackSelector(context);
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
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;
}
MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex);
releaseMediaDrmOfMediaSource(removedMediaSource);
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();
for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) {
mediaDrm.release();
}
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");
}
FrameworkMediaDrm mediaDrm = null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
if (drmConfiguration != null) {
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());
}
try {
mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid);
drmSessionManager =
new DefaultDrmSessionManager<>(
drmConfiguration.uuid,
mediaDrm,
drmCallback,
/* optionalKeyRequestParameters= */ null,
/* multiSession= */ true);
} catch (UnsupportedDrmException e) {
// Do nothing. The track selector will avoid selecting the DRM protected tracks.
}
}
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);
}
if (mediaDrm != null) {
mediaDrms.put(createdMediaSource, mediaDrm);
}
return createdMediaSource;
}
private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) {
FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource);
if (mediaDrmToRelease != null) {
mediaDrmToRelease.release();
}
}
}

View file

@ -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>

4
demos/gvr/README.md Normal file
View file

@ -0,0 +1,4 @@
# ExoPlayer VR player demo #
This folder contains a demo application that showcases 360 video playback using
ExoPlayer GVR extension.

59
demos/gvr/build.gradle Normal file
View file

@ -0,0 +1,59 @@
// 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.application'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
shrinkResources true
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'
}
}
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-gvr')
implementation 'androidx.annotation:annotation:1.1.0'
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View file

@ -0,0 +1,74 @@
<?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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.gvrdemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:largeHeap="true">
<activity
android:name="com.google.android.exoplayer2.gvrdemo.SampleChooserActivity"
android:configChanges="keyboardHidden"
android:exported="true"
android:label="@string/application_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.exolist\\.json"/>
</intent-filter>
</activity>
<activity
android:name="com.google.android.exoplayer2.gvrdemo.PlayerActivity"
android:configChanges="density|keyboardHidden|navigation|orientation|screenSize|uiMode"
android:enableVrMode="@string/gvr_vr_mode_component"
android:exported="false"
android:label="@string/application_name"
android:launchMode="singleTask"
android:resizeableActivity="false"
android:screenOrientation="landscape"
android:theme="@style/VrActivityTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="com.google.intent.category.CARDBOARD"/> <!-- copybara:strip(development-only) -->
<category android:name="com.google.intent.category.DAYDREAM"/>
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,242 @@
/*
* 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.gvrdemo;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity;
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.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
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.util.EventLogger;
import com.google.android.exoplayer2.util.Util;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends GvrPlayerActivity implements PlaybackPreparer {
public static final String EXTENSION_EXTRA = "extension";
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";
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
private TrackGroupArray lastSeenTrackGroupArray;
private boolean startAutoPlay;
private int startWindow;
private long startPosition;
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
dataSourceFactory =
new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent));
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
if (sphericalStereoMode != null) {
int stereoMode;
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_MONO;
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
} else {
showToast(R.string.error_unrecognized_stereo_mode);
finish();
return;
}
setDefaultStereoMode(stereoMode);
}
clearStartPosition();
}
@Override
public void onResume() {
super.onResume();
if (Util.SDK_INT <= 23 || player == null) {
initializePlayer();
}
}
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
releasePlayer();
}
}
@Override
public void onDestroy() {
super.onDestroy();
}
// PlaybackControlView.PlaybackPreparer implementation
@Override
public void preparePlayback() {
initializePlayer();
}
// Internal methods
private void initializePlayer() {
if (player == null) {
Intent intent = getIntent();
Uri uri = intent.getData();
if (!Util.checkCleartextTrafficPermitted(uri)) {
showToast(R.string.error_cleartext_not_permitted);
return;
}
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this);
trackSelector = new DefaultTrackSelector(/* context= */ this);
lastSeenTrackGroupArray = null;
player =
ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
setPlayer(player);
mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA));
}
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
if (haveStartPosition) {
player.seekTo(startWindow, startPosition);
}
player.prepare(mediaSource, !haveStartPosition, false);
}
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension);
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);
}
}
private void releasePlayer() {
if (player != null) {
updateStartPosition();
player.release();
player = null;
mediaSource = null;
trackSelector = null;
}
}
private void updateStartPosition() {
if (player != null) {
startAutoPlay = player.getPlayWhenReady();
startWindow = player.getCurrentWindowIndex();
startPosition = Math.max(0, player.getContentPosition());
}
}
private void clearStartPosition() {
startAutoPlay = true;
startWindow = C.INDEX_UNSET;
startPosition = C.TIME_UNSET;
}
private void showToast(int messageId) {
showToast(getString(messageId));
}
private void showToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
private class PlayerEventListener implements Player.EventListener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (player.getPlaybackError() != null) {
// The user has performed a seek whilst in the error state. Update the resume position so
// that if the user then retries, playback resumes from the position to which they seeked.
updateStartPosition();
}
}
@Override
public void onPlayerError(ExoPlaybackException e) {
updateStartPosition();
}
@Override
@SuppressWarnings("ReferenceEquality")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video);
}
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
}
}

View file

@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 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.gvrdemo;
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT;
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO;
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
/** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends Activity {
private final Sample[] samples =
new Sample[] {
new Sample(
"Congo (360 top-bottom stereo)",
"https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"Sphericalv2 (180 top-bottom stereo)",
"https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"Iceland (360 top-bottom stereo ts)",
"https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"Camera motion metadata test",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/synthetic_with_camm.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"actual_camera_cat",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/actual_camera_cat.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"johnny_stitched",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/johnny_stitched.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"lenovo_birds.vr",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/lenovo_birds.vr.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"mono_v1_sample",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/mono_v1_sample.mp4",
SPHERICAL_STEREO_MODE_MONO),
new Sample(
"not_vr180_actually_shot_with_moto_mod",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/"
+ "not_vr180_actually_shot_with_moto_mod.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"stereo_v1_sample",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/stereo_v1_sample.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"yi_giraffes.vr",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/yi_giraffes.vr.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_chooser_activity);
ListView sampleListView = findViewById(R.id.sample_list);
sampleListView.setAdapter(
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples));
sampleListView.setOnItemClickListener(
(parent, view, position, id) ->
startActivity(
samples[position].buildIntent(/* context= */ SampleChooserActivity.this)));
}
private static final class Sample {
public final String name;
public final String uri;
public final String extension;
public final String sphericalStereoMode;
public Sample(String name, String uri, String sphericalStereoMode) {
this(name, uri, sphericalStereoMode, null);
}
public Sample(String name, String uri, String sphericalStereoMode, String extension) {
this.name = name;
this.uri = uri;
this.extension = extension;
this.sphericalStereoMode = sphericalStereoMode;
}
public Intent buildIntent(Context context) {
Intent intent = new Intent(context, PlayerActivity.class);
return intent
.setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
}
@Override
public String toString() {
return name;
}
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (C) 2016 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.
@ -13,5 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<manifest package="com.google.android.exoplayer2.testutil"/>
<ListView android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,28 @@
<?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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="application_name">ExoPlayer VR Demo</string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</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>

View file

@ -53,7 +53,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View file

@ -62,7 +62,7 @@ android {
}
dependencies {
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0'
implementation 'androidx.fragment:fragment:1.0.0'
implementation 'com.google.android.material:material:1.0.0'

View file

@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService {
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID,
R.string.exo_download_notification_channel_name);
R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
}

View file

@ -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();
}
@ -123,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);
}
@ -202,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,
@ -212,9 +215,7 @@ public class DownloadTracker {
@Override
public void onPrepareError(DownloadHelper helper, IOException e) {
Toast.makeText(
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
.show();
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
Log.e(TAG, "Failed to start download", e);
}
@ -229,7 +230,7 @@ public class DownloadTracker {
downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex,
/* rendererIndex= */ i,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
}
}

View file

@ -38,7 +38,9 @@ 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.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
@ -77,41 +79,48 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList;
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 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 AD_TAG_URI_EXTRA = "ad_tag_uri";
// 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";
@ -123,6 +132,8 @@ public class PlayerActivity extends AppCompatActivity
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
private final ArrayList<FrameworkMediaDrm> mediaDrms;
private PlayerView playerView;
private LinearLayout debugRootView;
private Button selectTracksButton;
@ -131,7 +142,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;
@ -147,11 +157,16 @@ public class PlayerActivity extends AppCompatActivity
private AdsLoader adsLoader;
private Uri loadedAdTagUri;
public PlayerActivity() {
mediaDrms = new ArrayList<>();
}
// Activity lifecycle
@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);
}
@ -193,7 +208,7 @@ public class PlayerActivity extends AppCompatActivity
startWindow = savedInstanceState.getInt(KEY_WINDOW);
startPosition = savedInstanceState.getLong(KEY_POSITION);
} else {
trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this);
clearStartPosition();
}
}
@ -327,67 +342,11 @@ 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;
}
releaseMediaDrms();
mediaSource = createTopLevelMediaSource(intent);
if (mediaSource == null) {
return;
}
TrackSelection.Factory trackSelectionFactory;
@ -407,13 +366,12 @@ 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);
ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
@ -421,28 +379,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;
@ -453,26 +391,130 @@ 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]);
}
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) {
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
Sample.DrmInfo drmInfo = parameters.drmInfo;
if (drmInfo != null) {
int errorStringId = R.string.error_drm_unknown;
if (Util.SDK_INT < 18) {
errorStringId = R.string.error_drm_not_supported;
} else {
try {
if (drmInfo.drmScheme == null) {
errorStringId = R.string.error_drm_unsupported_scheme;
} else {
drmSessionManager =
buildDrmSessionManagerV18(
drmInfo.drmScheme,
drmInfo.drmLicenseUrl,
drmInfo.drmKeyRequestProperties,
drmInfo.drmMultiSession);
}
} 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 null;
}
} else {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
}
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<FrameworkMediaCrypto> 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);
}
@ -491,8 +533,9 @@ public class PlayerActivity extends AppCompatActivity
keyRequestPropertiesArray[i + 1]);
}
}
releaseMediaDrm();
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
mediaDrms.add(mediaDrm);
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
}
@ -510,14 +553,14 @@ public class PlayerActivity extends AppCompatActivity
if (adsLoader != null) {
adsLoader.setPlayer(null);
}
releaseMediaDrm();
releaseMediaDrms();
}
private void releaseMediaDrm() {
if (mediaDrm != null) {
private void releaseMediaDrms() {
for (FrameworkMediaDrm mediaDrm : mediaDrms) {
mediaDrm.release();
mediaDrm = null;
}
mediaDrms.clear();
}
private void releaseAdsLoader() {
@ -555,7 +598,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 {
@ -570,12 +614,12 @@ public class PlayerActivity extends AppCompatActivity
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
adsLoader = loaderConstructor.newInstance(this, adTagUri);
}
adsLoader.setPlayer(player);
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
@ -678,7 +722,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) {
@ -693,12 +737,11 @@ public class PlayerActivity extends AppCompatActivity
errorString =
getString(
R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
decoderInitializationException.codecInfo.name);
}
}
}
return Pair.create(0, errorString);
}
}
}

View file

@ -0,0 +1,187 @@
/*
* 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.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);
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
return new UriSample(
/* name= */ null,
DrmInfo.createFromIntent(intent, extrasKeySuffix),
uri,
extension,
adTagUri,
/* sphericalStereoMode= */ null);
}
public final Uri uri;
public final String extension;
public final DrmInfo drmInfo;
public final Uri adTagUri;
public final String sphericalStereoMode;
public UriSample(
String name,
DrmInfo drmInfo,
Uri uri,
String extension,
Uri adTagUri,
String sphericalStereoMode) {
super(name);
this.uri = uri;
this.extension = extension;
this.drmInfo = drmInfo;
this.adTagUri = adTagUri;
this.sphericalStereoMode = sphericalStereoMode;
}
@Override
public void addToIntent(Intent intent) {
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
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());
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);
}
}
}
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 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);
}

View file

@ -38,6 +38,9 @@ import android.widget.TextView;
import android.widget.Toast;
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;
@ -161,13 +164,17 @@ 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);
sample.addToIntent(intent);
startActivity(intent);
return true;
}
@ -309,17 +316,12 @@ 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 "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()) {
@ -357,17 +359,21 @@ public class SampleChooserActivity extends AppCompatActivity
DrmInfo drmInfo =
drmScheme == null
? null
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
: new DrmInfo(
Util.getDrmUuid(drmScheme),
drmLicenseUrl,
drmKeyRequestProperties,
drmMultiSession);
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,
adTagUri != null ? Uri.parse(adTagUri) : null,
sphericalStereoMode);
}
}
@ -497,116 +503,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);
}
}
}

View file

@ -306,7 +306,7 @@ public final class TrackSelectionDialog extends DialogFragment {
}
}
/** Fragment to show a track seleciton in tab of the track selection dialog. */
/** Fragment to show a track selection in tab of the track selection dialog. */
public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener {

View file

@ -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>

View file

@ -31,13 +31,14 @@ android {
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
implementation 'androidx.annotation:annotation:1.0.2'
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
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 {

View file

@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* {@link Player} implementation that communicates with a Cast receiver app.
@ -80,17 +83,18 @@ public final class CastPlayer extends BasePlayer {
private final CastTimelineTracker timelineTracker;
private final Timeline.Period period;
private RemoteMediaClient remoteMediaClient;
// Result callbacks.
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
// Listeners.
private final CopyOnWriteArraySet<EventListener> listeners;
private SessionAvailabilityListener sessionAvailabilityListener;
// Listeners and notification.
private final CopyOnWriteArrayList<ListenerHolder> listeners;
private final ArrayList<ListenerNotificationTask> notificationsBatch;
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
@ -113,7 +117,9 @@ public final class CastPlayer extends BasePlayer {
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
listeners = new CopyOnWriteArraySet<>();
listeners = new CopyOnWriteArrayList<>();
notificationsBatch = new ArrayList<>();
ongoingNotificationsTasks = new ArrayDeque<>();
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
@ -141,6 +147,7 @@ public final class CastPlayer extends BasePlayer {
* starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
@Nullable
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
}
@ -156,8 +163,9 @@ public final class CastPlayer extends BasePlayer {
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
long positionMs, @RepeatMode int repeatMode) {
@Nullable
public PendingResult<MediaChannelResult> loadItems(
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
@ -173,6 +181,7 @@ public final class CastPlayer extends BasePlayer {
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
@Nullable
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
@ -187,6 +196,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
@Nullable
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
@ -204,6 +214,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
@Nullable
public PendingResult<MediaChannelResult> removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
@ -222,6 +233,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
@Nullable
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
@ -239,6 +251,7 @@ public final class CastPlayer extends BasePlayer {
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
@Nullable
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
@ -257,9 +270,9 @@ public final class CastPlayer extends BasePlayer {
/**
* Sets a listener for updates on the cast session availability.
*
* @param listener The {@link SessionAvailabilityListener}.
* @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener;
}
@ -296,12 +309,17 @@ public final class CastPlayer extends BasePlayer {
@Override
public void addListener(EventListener listener) {
listeners.add(listener);
listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(EventListener listener) {
listeners.remove(listener);
for (ListenerHolder listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release();
listeners.remove(listenerHolder);
}
}
}
@Override
@ -311,6 +329,7 @@ public final class CastPlayer extends BasePlayer {
}
@Override
@Nullable
public ExoPlaybackException getPlaybackError() {
return null;
}
@ -348,14 +367,13 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
}
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
} else if (pendingSeekCount == 0) {
for (EventListener listener : listeners) {
listener.onSeekProcessed();
}
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
}
flushNotifications();
}
@Override
@ -442,11 +460,6 @@ public final class CastPlayer extends BasePlayer {
return currentTimeline;
}
@Override
@Nullable public Object getCurrentManifest() {
return null;
}
@Override
public int getCurrentPeriodIndex() {
return getCurrentWindowIndex();
@ -519,7 +532,7 @@ public final class CastPlayer extends BasePlayer {
// Internal methods.
public void updateInternalState() {
private void updateInternalState() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
@ -531,30 +544,40 @@ public final class CastPlayer extends BasePlayer {
|| this.playWhenReady != playWhenReady) {
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
for (EventListener listener : listeners) {
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
}
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
for (EventListener listener : listeners) {
listener.onRepeatModeChanged(repeatMode);
}
}
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
}
}
if (updateTracksAndSelections()) {
for (EventListener listener : listeners) {
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
}
notificationsBatch.add(
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
}
maybeUpdateTimelineAndNotify();
int currentWindowIndex = C.INDEX_UNSET;
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
if (currentItem != null) {
currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
}
if (currentWindowIndex == C.INDEX_UNSET) {
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
currentWindowIndex = 0;
}
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
notificationsBatch.add(
new ListenerNotificationTask(
listener ->
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
}
if (updateTracksAndSelections()) {
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
}
flushNotifications();
}
private void maybeUpdateTimelineAndNotify() {
@ -562,9 +585,9 @@ public final class CastPlayer extends BasePlayer {
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
for (EventListener listener : listeners) {
listener.onTimelineChanged(currentTimeline, null, reason);
}
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onTimelineChanged(currentTimeline, reason)));
}
}
@ -654,7 +677,8 @@ public final class CastPlayer extends BasePlayer {
}
}
private @Nullable MediaStatus getMediaStatus() {
@Nullable
private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
}
@ -702,16 +726,6 @@ public final class CastPlayer extends BasePlayer {
}
}
/**
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
* there is no media session, returns 0.
*/
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
Integer currentItemId = mediaStatus != null
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
return currentItemId != null ? currentItemId : 0;
}
private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) {
@ -827,7 +841,23 @@ public final class CastPlayer extends BasePlayer {
}
// Result callbacks hooks.
// 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> {
@ -841,9 +871,25 @@ public final class CastPlayer extends BasePlayer {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
for (EventListener listener : listeners) {
listener.onSeekProcessed();
}
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
flushNotifications();
}
}
}
private final class ListenerNotificationTask {
private final Iterator<ListenerHolder> listenersSnapshot;
private final ListenerInvocation listenerInvocation;
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
this.listenersSnapshot = listeners.iterator();
this.listenerInvocation = listenerInvocation;
}
public void execute() {
while (listenersSnapshot.hasNext()) {
listenersSnapshot.next().invoke(listenerInvocation);
}
}
}

View file

@ -117,6 +117,7 @@ import java.util.Arrays;
Object tag = setTag ? ids[windowIndex] : null;
return window.set(
tag,
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,

View file

@ -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;
}

View file

@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
import java.util.Collections;
import java.util.List;
/**
@ -27,16 +28,38 @@ 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
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null;
return Collections.emptyList();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -31,11 +31,13 @@ android {
}
dependencies {
api 'org.chromium.net:cronet-embedded:73.3683.76'
api 'org.chromium.net:cronet-embedded:75.3770.101'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
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 {

View file

@ -15,6 +15,8 @@
*/
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;
@ -41,6 +43,7 @@ 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;
@ -113,16 +116,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 +134,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 +159,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 +243,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 +257,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 +274,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 +287,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
RequestProperties defaultRequestProperties) {
@Nullable RequestProperties defaultRequestProperties) {
this(
cronetEngine,
executor,
@ -214,9 +295,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
Clock.DEFAULT,
defaultRequestProperties,
false);
/* handleSetCookieRequests= */ false);
}
/**
@ -232,10 +312,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 +327,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 +364,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
@ -305,6 +398,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
@Override
@Nullable
public Uri getUri() {
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
}
@ -317,22 +411,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();
@ -340,6 +435,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 =
@ -347,7 +443,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));
}
@ -355,11 +451,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);
}
}
@ -369,7 +466,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 {
@ -378,7 +475,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;
@ -397,37 +494,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 {
@ -452,6 +531,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) {
@ -524,7 +712,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)) {
@ -553,7 +741,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");
@ -631,10 +861,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
@ -643,13 +885,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;
}
@ -658,40 +902,46 @@ 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);
} 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

View file

@ -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,

View file

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

View file

@ -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;

View file

@ -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>

View file

@ -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;
@ -85,7 +84,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 +93,19 @@ public final class CronetDataSourceTest {
private boolean redirectCalled;
@Before
public void setUp() throws Exception {
public void setUp() {
MockitoAnnotations.initMocks(this);
dataSourceUnderTest =
new CronetDataSource(
mockCronetEngine,
mockExecutor,
mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
/* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
null,
false);
/* defaultRequestProperties= */ null,
/* handleSetCookieRequests= */ false);
dataSourceUnderTest.addTransferListener(mockTransferListener);
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder);
@ -283,7 +279,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 +294,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 +554,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 +991,6 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
@ -765,13 +1021,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 +1059,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 +1109,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 +1170,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 +1379,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;
}
}

View file

@ -38,9 +38,10 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {

View file

@ -92,8 +92,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
Format format) {
protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
@ -113,7 +113,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@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;

View file

@ -42,7 +42,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;
@ -172,28 +172,49 @@ import java.util.List;
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS:
return initializationData.get(0);
case MimeTypes.AUDIO_ALAC:
return getAlacExtraData(initializationData);
case MimeTypes.AUDIO_VORBIS:
byte[] header0 = initializationData.get(0);
byte[] header1 = initializationData.get(1);
byte[] extraData = new byte[header0.length + header1.length + 6];
extraData[0] = (byte) (header0.length >> 8);
extraData[1] = (byte) (header0.length & 0xFF);
System.arraycopy(header0, 0, extraData, 2, header0.length);
extraData[header0.length + 2] = 0;
extraData[header0.length + 3] = 0;
extraData[header0.length + 4] = (byte) (header1.length >> 8);
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData;
return getVorbisExtraData(initializationData);
default:
// Other codecs do not require extra data.
return null;
}
}
private static byte[] getAlacExtraData(List<byte[]> initializationData) {
// FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra
// data. initializationData[0] contains only the magic cookie, and so we need to package it into
// an ALAC atom. See:
// https://ffmpeg.org/doxygen/0.6/alac_8c.html
// https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt
byte[] magicCookie = initializationData.get(0);
int alacAtomLength = 12 + magicCookie.length;
ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);
alacAtom.putInt(alacAtomLength);
alacAtom.putInt(0x616c6163); // type=alac
alacAtom.putInt(0); // version=0, flags=0
alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);
return alacAtom.array();
}
private static byte[] getVorbisExtraData(List<byte[]> initializationData) {
byte[] header0 = initializationData.get(0);
byte[] header1 = initializationData.get(1);
byte[] extraData = new byte[header0.length + header1.length + 6];
extraData[0] = (byte) (header0.length >> 8);
extraData[1] = (byte) (header0.length & 0xFF);
System.arraycopy(header0, 0, extraData, 2, header0.length);
extraData[header0.length + 2] = 0;
extraData[header0.length + 3] = 0;
extraData[header0.length + 4] = (byte) (header1.length >> 8);
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData;
}
private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,

View file

@ -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;

View file

@ -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>

View file

@ -39,10 +39,12 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {

View file

@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*;
}
-keep class com.google.android.exoplayer2.util.FlacStreamInfo {
-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
*;
}
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*;
}

View file

@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue();

View file

@ -228,7 +228,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);

View file

@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
public class FlacExtractorTest {
@Before
public void setUp() throws Exception {
public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}

View file

@ -82,7 +82,7 @@ public class FlacPlaybackTest {
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =

View file

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo,
FlacStreamMetadata streamMetadata,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
super(
new FlacSeekTimestampConverter(streamInfo),
new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni),
streamInfo.durationUs(),
streamMetadata.durationUs(),
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamInfo.totalSamples,
/* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position.
*/
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo;
private final FlacStreamMetadata streamMetadata;
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
this.streamMetadata = streamMetadata;
}
@Override
public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
}
}
}

View file

@ -15,11 +15,13 @@
*/
package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
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.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
@ -56,21 +58,20 @@ import java.util.List;
}
decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamInfo streamInfo;
FlacStreamMetadata streamMetadata;
try {
streamInfo = decoderJni.decodeMetadata();
streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) {
throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) {
// Never happens.
throw new IllegalStateException(e);
}
if (streamInfo == null) {
throw new FlacDecoderException("Metadata decoding failed");
}
int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
}
@Override
@ -94,6 +95,7 @@ import java.util.List;
}
@Override
@Nullable
protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {

View file

@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
}
}
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext;
private ByteBuffer byteBufferData;
private ExtractorInput extractorInput;
@Nullable private ByteBuffer byteBufferData;
@Nullable private ExtractorInput extractorInput;
@Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput;
private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) {
@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
}
/**
* Sets data to be parsed by libflac.
* @param byteBufferData Source {@link ByteBuffer}
* Sets the data to be parsed.
*
* @param byteBufferData Source {@link ByteBuffer}.
*/
public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData;
this.extractorInput = null;
this.tempBuffer = null;
}
/**
* Sets data to be parsed by libflac.
* @param extractorInput Source {@link ExtractorInput}
* Sets the data to be parsed.
*
* @param extractorInput Source {@link ExtractorInput}.
*/
public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null;
this.extractorInput = extractorInput;
if (tempBuffer == null) {
this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
endOfExtractorInput = false;
if (tempBuffer == null) {
tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
}
/**
* Returns whether the end of the data to be parsed has been reached, or true if no data was set.
*/
public boolean isEndOfData() {
if (byteBufferData != null) {
return byteBufferData.remaining() == 0;
} else if (extractorInput != null) {
return endOfExtractorInput;
} else {
return true;
}
return true;
}
/** Clears the data to be parsed. */
public void clearData() {
byteBufferData = null;
extractorInput = null;
}
/**
* Reads up to {@code length} bytes from the data source.
* <p>
* This method blocks until at least one byte of data can be read, the end of the input is
*
* <p>This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown.
* <p>
* This method is called from the native code.
*
* @param target A target {@link ByteBuffer} into which data should be written.
* @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns
* zero; it just means all the data read from the source.
* @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
* read from the source, then 0 is returned.
*/
@SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining();
if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount);
target.put(byteBufferData);
byteBufferData.limit(originalLimit);
} else if (extractorInput != null) {
ExtractorInput extractorInput = this.extractorInput;
byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
int read = readFromExtractorInput(0, byteCount);
int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
// the buffer of the input. Do another read to reduce the number of calls to this method
// from the native code.
read += readFromExtractorInput(read, byteCount - read);
read +=
readFromExtractorInput(
extractorInput, tempBuffer, read, /* length= */ byteCount - read);
}
byteCount = read;
target.put(tempBuffer, 0, byteCount);
@ -127,9 +142,13 @@ import java.nio.ByteBuffer;
return byteCount;
}
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext);
/** Decodes and consumes the metadata from the FLAC stream. */
public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
if (streamMetadata == null) {
throw new ParserException("Failed to decode stream metadata");
}
return streamMetadata;
}
/**
@ -234,7 +253,8 @@ import java.nio.ByteBuffer;
flacRelease(nativeDecoderContext);
}
private int readFromExtractorInput(int offset, int length)
private int readFromExtractorInput(
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
throws IOException, InterruptedException {
int read = extractorInput.read(tempBuffer, offset, length);
if (read == C.RESULT_END_OF_INPUT) {
@ -246,7 +266,7 @@ import java.nio.ByteBuffer;
private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context)
private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)

View file

@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -33,7 +33,8 @@ import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
@ -42,6 +43,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Facilitates the extraction of data from the FLAC container format.
@ -74,23 +78,20 @@ public final class FlacExtractor implements Extractor {
*/
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
private final boolean isId3MetadataDisabled;
private final boolean id3MetadataDisabled;
private FlacDecoderJni decoderJni;
@Nullable private FlacDecoderJni decoderJni;
private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput;
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
private boolean streamMetadataDecoded;
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
private boolean readPastStreamInfo;
@Nullable private Metadata id3Metadata;
@Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override
@ -130,48 +132,53 @@ public final class FlacExtractor implements Extractor {
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
}
decoderJni.setData(input);
readPastStreamInfo(input);
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition);
}
long lastDecodePosition = decoderJni.getDecodePosition();
FlacDecoderJni decoderJni = initDecoderJni(input);
try {
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
decodeStreamMetadata(input);
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
}
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
long lastDecodePosition = decoderJni.getDecodePosition();
try {
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
} finally {
decoderJni.clearData();
}
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
readPastStreamInfo = false;
streamMetadataDecoded = false;
}
if (decoderJni != null) {
decoderJni.reset(position);
}
if (flacBinarySearchSeeker != null) {
flacBinarySearchSeeker.setSeekTargetUs(timeUs);
if (binarySearchSeeker != null) {
binarySearchSeeker.setSeekTargetUs(timeUs);
}
}
@Override
public void release() {
flacBinarySearchSeeker = null;
binarySearchSeeker = null;
if (decoderJni != null) {
decoderJni.release();
decoderJni = null;
@ -179,123 +186,141 @@ public final class FlacExtractor implements Extractor {
}
/**
* Peeks ID3 tag data (if present) at the beginning of the input.
* Peeks ID3 tag data at the beginning of the input.
*
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
* present in the input.
* @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate =
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
@EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
private FlacDecoderJni initDecoderJni(ExtractorInput input) {
FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
decoderJni.setData(input);
return decoderJni;
}
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
if (streamMetadataDecoded) {
return;
}
FlacStreamMetadata streamMetadata;
try {
streamMetadata = decoderJni.decodeStreamMetadata();
} catch (IOException e) {
decoderJni.reset(/* newPosition= */ 0);
input.setRetryPosition(/* position= */ 0, e);
throw e;
}
streamMetadataDecoded = true;
if (this.streamMetadata == null) {
this.streamMetadata = streamMetadata;
binarySearchSeeker =
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
if (streamMetadata.metadata != null) {
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
}
}
@RequiresNonNull("binarySearchSeeker")
private int handlePendingSeek(
ExtractorInput input,
PositionHolder seekPosition,
ParsableByteArray outputBuffer,
OutputFrameHolder outputFrameHolder,
TrackOutput trackOutput)
throws InterruptedException, IOException {
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
}
return seekResult;
}
/**
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
*
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
*/
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
private static boolean peekFlacSignature(ExtractorInput input)
throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
input.peekFully(header, 0, FLAC_SIGNATURE.length);
input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
}
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
if (readPastStreamInfo) {
return;
}
FlacStreamInfo streamInfo = decodeStreamInfo(input);
readPastStreamInfo = true;
if (this.streamInfo == null) {
updateFlacStreamInfo(input, streamInfo);
}
}
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
outputSeekMap(input, streamInfo);
outputFormat(streamInfo);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
throws InterruptedException, IOException {
try {
FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
if (streamInfo == null) {
throw new IOException("Metadata decoding failed");
}
return streamInfo;
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
throw e;
}
}
private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
SeekMap seekMap =
hasSeekTable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: getSeekMapForNonSeekTableFlac(input, streamInfo);
extractorOutput.seekMap(seekMap);
}
private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
long inputLength = input.getLength();
if (inputLength != C.LENGTH_UNSET) {
/**
* Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
* handle seeks.
*/
@Nullable
private static FlacBinarySearchSeeker outputSeekMap(
FlacDecoderJni decoderJni,
FlacStreamMetadata streamMetadata,
long streamLength,
ExtractorOutput output) {
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
FlacBinarySearchSeeker binarySearchSeeker = null;
SeekMap seekMap;
if (hasSeekTable) {
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
} else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
flacBinarySearchSeeker =
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
return flacBinarySearchSeeker.getSeekMap();
} else { // can't seek at all, because there's no SeekTable and the input length is unknown.
return new SeekMap.Unseekable(streamInfo.durationUs());
binarySearchSeeker =
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
seekMap = binarySearchSeeker.getSeekMap();
} else {
seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
}
output.seekMap(seekMap);
return binarySearchSeeker;
}
private void outputFormat(FlacStreamInfo streamInfo) {
private static void outputFormat(
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
streamInfo.bitRate(),
streamInfo.maxDecodedFrameSize(),
streamInfo.channels,
streamInfo.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample),
streamMetadata.bitRate(),
streamMetadata.maxDecodedFrameSize(),
streamMetadata.channels,
streamMetadata.sampleRate,
getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat);
metadata);
output.format(mediaFormat);
}
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
throws InterruptedException, IOException {
int seekResult =
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}
private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
outputBuffer.setPosition(0);
trackOutput.sampleData(outputBuffer, size);
trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
private static void outputSample(
ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
sampleData.setPosition(0);
output.sampleData(sampleData, size);
output.sampleMetadata(
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
}
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */

View file

@ -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,15 @@ 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) {
protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
@ -65,7 +66,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);

View file

@ -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;

View file

@ -14,9 +14,12 @@
* limitations under the License.
*/
#include <jni.h>
#include <android/log.h>
#include <jni.h>
#include <cstdlib>
#include <cstring>
#include "include/flac_parser.h"
#define LOG_TAG "flac_jni"
@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL;
}
jclass arrayListClass = env->FindClass("java/util/ArrayList");
jmethodID arrayListConstructor =
env->GetMethodID(arrayListClass, "<init>", "()V");
jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
jmethodID arrayListAddMethod =
env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
if (context->parser->areVorbisCommentsValid()) {
std::vector<std::string> vorbisComments =
context->parser->getVorbisComments();
for (std::vector<std::string>::const_iterator vorbisComment =
vorbisComments.begin();
vorbisComment != vorbisComments.end(); ++vorbisComment) {
jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
env->DeleteLocalRef(commentString);
}
}
jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
bool picturesValid = context->parser->arePicturesValid();
if (picturesValid) {
std::vector<FlacPicture> pictures = context->parser->getPictures();
jclass pictureFrameClass = env->FindClass(
"com/google/android/exoplayer2/metadata/flac/PictureFrame");
jmethodID pictureFrameConstructor =
env->GetMethodID(pictureFrameClass, "<init>",
"(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
for (std::vector<FlacPicture>::const_iterator picture = pictures.begin();
picture != pictures.end(); ++picture) {
jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
jstring description = env->NewStringUTF(picture->description.c_str());
jbyteArray pictureData = env->NewByteArray(picture->data.size());
env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
(signed char *)&picture->data[0]);
jobject pictureFrame = env->NewObject(
pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
description, picture->width, picture->height, picture->depth,
picture->colors, pictureData);
env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
env->DeleteLocalRef(mimeType);
env->DeleteLocalRef(description);
env->DeleteLocalRef(pictureData);
}
}
const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo();
jclass cls = env->FindClass(
jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/"
"FlacStreamInfo");
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIIIIIIJ)V");
"FlacStreamMetadata");
jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
return env->NewObject(cls, constructor, streamInfo.min_blocksize,
streamInfo.max_blocksize, streamInfo.min_framesize,
streamInfo.max_framesize, streamInfo.sample_rate,
streamInfo.channels, streamInfo.bits_per_sample,
streamInfo.total_samples);
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.min_blocksize, streamInfo.max_blocksize,
streamInfo.min_framesize, streamInfo.max_framesize,
streamInfo.sample_rate, streamInfo.channels,
streamInfo.bits_per_sample, streamInfo.total_samples,
commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {

View file

@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table;
break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (!mVorbisCommentsValid) {
FLAC__StreamMetadata_VorbisComment vorbisComment =
metadata->data.vorbis_comment;
for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
vorbisComment.comments[i];
if (vorbisCommentEntry.entry != NULL) {
std::string comment(
reinterpret_cast<char *>(vorbisCommentEntry.entry),
vorbisCommentEntry.length);
mVorbisComments.push_back(comment);
}
}
mVorbisCommentsValid = true;
} else {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
case FLAC__METADATA_TYPE_PICTURE: {
const FLAC__StreamMetadata_Picture *parsedPicture =
&metadata->data.picture;
FlacPicture picture;
picture.mimeType.assign(std::string(parsedPicture->mime_type));
picture.description.assign(
std::string((char *)parsedPicture->description));
picture.data.assign(parsedPicture->data,
parsedPicture->data + parsedPicture->data_length);
picture.width = parsedPicture->width;
picture.height = parsedPicture->height;
picture.depth = parsedPicture->depth;
picture.colors = parsedPicture->colors;
picture.type = parsedPicture->type;
mPictures.push_back(picture);
mPicturesValid = true;
break;
}
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL),
mEOF(false),
mStreamInfoValid(false),
mVorbisCommentsValid(false),
mPicturesValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,

View file

@ -19,6 +19,10 @@
#include <stdint.h>
#include <cstdlib>
#include <string>
#include <vector>
// libFLAC parser
#include "FLAC/stream_decoder.h"
@ -26,6 +30,17 @@
typedef int status_t;
struct FlacPicture {
int type;
std::string mimeType;
std::string description;
FLAC__uint32 width;
FLAC__uint32 height;
FLAC__uint32 depth;
FLAC__uint32 colors;
std::vector<char> data;
};
class FLACParser {
public:
FLACParser(DataSource *source);
@ -44,6 +59,16 @@ class FLACParser {
return mStreamInfo;
}
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
const std::vector<std::string>& getVorbisComments() const {
return mVorbisComments;
}
bool arePicturesValid() const { return mPicturesValid; }
const std::vector<FlacPicture> &getPictures() const { return mPictures; }
int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
@ -71,6 +96,10 @@ class FLACParser {
mEOF = false;
if (newPosition == 0) {
mStreamInfoValid = false;
mVorbisCommentsValid = false;
mPicturesValid = false;
mVorbisComments.clear();
mPictures.clear();
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
@ -116,6 +145,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset;
// cached when the VORBIS_COMMENT metadata is parsed by libFLAC
std::vector<std::string> mVorbisComments;
bool mVorbisCommentsValid;
// cached when the PICTURE metadata is parsed by libFLAC
std::vector<FlacPicture> mPictures;
bool mPicturesValid;
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;

View file

@ -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>

View file

@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}

View file

@ -50,7 +50,10 @@ 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. */
/**
* Base activity for VR 360 video playback. Before starting the video playback a player needs to be
* set using {@link #setPlayer(Player)}.
*/
public abstract class GvrPlayerActivity extends GvrActivity {
private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
@ -58,12 +61,12 @@ public abstract class GvrPlayerActivity extends GvrActivity {
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;
private @MonotonicNonNull GlViewGroup glView;
private @MonotonicNonNull ControllerManager controllerManager;
private @MonotonicNonNull SurfaceTexture surfaceTexture;
private @MonotonicNonNull Surface surface;
private @MonotonicNonNull SceneRenderer scene;
private @MonotonicNonNull PlayerControlView playerControl;
public GvrPlayerActivity() {
mainHandler = new Handler(Looper.getMainLooper());

View file

@ -32,10 +32,12 @@ android {
}
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {

View file

@ -313,14 +313,14 @@ public final class ImaAdsLoader
*/
private static final int IMA_AD_STATE_PAUSED = 2;
private final @Nullable Uri adTagUri;
private final @Nullable String adsResponse;
@Nullable private final Uri adTagUri;
@Nullable private final String adsResponse;
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
private final boolean focusSkipButtonWhenAvailable;
private final int mediaBitrate;
private final @Nullable Set<UiElement> adUiElements;
private final @Nullable AdEventListener adEventListener;
@Nullable private final Set<UiElement> adUiElements;
@Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory;
private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks;
@ -426,7 +426,7 @@ public final class ImaAdsLoader
* @deprecated Use {@link ImaAdsLoader.Builder}.
*/
@Deprecated
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) {
this(
context,
adTagUri,
@ -946,8 +946,7 @@ public final class ImaAdsLoader
// Player.EventListener implementation.
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
if (timeline.isEmpty()) {
// The player is being reset or contains no media.
return;
@ -1054,13 +1053,8 @@ public final class ImaAdsLoader
long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
if (adGroupIndexForPosition == 0) {
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There is no preroll and midroll pod indices start at 1.
podIndexOffset = -1;
} else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position.
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
// Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
@ -1070,9 +1064,18 @@ public final class ImaAdsLoader
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
}
// We're removing one or more ads, which means that the earliest ad (if any) will be a
// midroll/postroll. Midroll pod indices start at 1.
// IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
// Store an index offset as we want to index all ads (including skipped ones) from 0.
if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
// We are playing a preroll.
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There's no ad to play which means there's no preroll.
podIndexOffset = -1;
} else {
// We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1;
}

View file

@ -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.ima;
import com.google.android.exoplayer2.util.NonNullApi;

View file

@ -13,4 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.ima.test" />
<manifest package="com.google.android.exoplayer2.ext.ima.test">
<uses-sdk/>
</manifest>

View file

@ -51,9 +51,7 @@ import java.util.ArrayList;
public void updateTimeline(Timeline timeline) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(
timeline,
null,
prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
}
prepared = true;
}

View file

@ -252,7 +252,8 @@ public class ImaAdsLoaderTest {
}
@Override
public @Nullable Ad getAd() {
@Nullable
public Ad getAd() {
return ad;
}

View file

@ -1,7 +1,11 @@
# ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md

View file

@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
*
* @see <a
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
* @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
* com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/
@Deprecated
public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false;

View file

@ -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.jobdispatcher;
import com.google.android.exoplayer2.util.NonNullApi;

View file

@ -32,7 +32,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.leanback:leanback:1.0.0'
}

View file

@ -51,10 +51,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
private final ComponentListener componentListener;
private final int updatePeriodMs;
private @Nullable PlaybackPreparer playbackPreparer;
@Nullable private PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
@Nullable private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@ -288,8 +288,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);

View file

@ -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.leanback;
import com.google.android.exoplayer2.util.NonNullApi;

View file

@ -33,6 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
api 'androidx.media:media:1.0.1'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {

View file

@ -52,6 +52,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/**
* Connects a {@link MediaSessionCompat} to a {@link Player}.
@ -172,7 +173,7 @@ public final class MediaSessionConnector {
ResultReceiver cb);
}
/** Interface to which playback preparation actions are delegated. */
/** Interface to which playback preparation and play actions are delegated. */
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS =
@ -197,14 +198,36 @@ public final class MediaSessionConnector {
* @return The bitmask of the supported media actions.
*/
long getSupportedPrepareActions();
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
void onPrepare();
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
void onPrepareFromMediaId(String mediaId, Bundle extras);
/** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
void onPrepareFromSearch(String query, Bundle extras);
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
void onPrepareFromUri(Uri uri, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepare()}.
*
* @param playWhenReady Whether playback should be started after preparation.
*/
void onPrepare(boolean playWhenReady);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
*
* @param mediaId The media id of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
*
* @param query The search query.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
*
* @param uri The {@link Uri} of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
}
/**
@ -337,7 +360,7 @@ public final class MediaSessionConnector {
* @param extras Optional extras sent by a media controller.
*/
void onCustomAction(
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras);
Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
/**
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
@ -355,6 +378,13 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
* <p>An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
* case the app should return a {@link MediaMetadataCompat} object that does not contain these
* resources as a placeholder. The app should start an asynchronous operation to download the
* bitmap and put it into a cache. Finally, the app should call {@link
* #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
* can now return a {@link MediaMetadataCompat} object with all the resources included.
*
* @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
@ -528,7 +558,7 @@ public final class MediaSessionConnector {
*
* @param queueNavigator The queue navigator.
*/
public void setQueueNavigator(QueueNavigator queueNavigator) {
public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) {
if (this.queueNavigator != queueNavigator) {
unregisterCommandReceiver(this.queueNavigator);
this.queueNavigator = queueNavigator;
@ -541,7 +571,7 @@ public final class MediaSessionConnector {
*
* @param queueEditor The queue editor.
*/
public void setQueueEditor(QueueEditor queueEditor) {
public void setQueueEditor(@Nullable QueueEditor queueEditor) {
if (this.queueEditor != queueEditor) {
unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor;
@ -643,7 +673,7 @@ public final class MediaSessionConnector {
mediaMetadataProvider != null && player != null
? mediaMetadataProvider.getMetadata(player)
: METADATA_EMPTY;
mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY);
mediaSession.setMetadata(metadata);
}
/**
@ -654,6 +684,7 @@ public final class MediaSessionConnector {
*/
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
@Nullable Player player = this.player;
if (player == null) {
builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
mediaSession.setPlaybackState(builder.build());
@ -662,6 +693,7 @@ public final class MediaSessionConnector {
Map<String, CustomActionProvider> currentActions = new HashMap<>();
for (CustomActionProvider customActionProvider : customActionProviders) {
@Nullable
PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
if (customAction != null) {
currentActions.put(customAction.getAction(), customActionProvider);
@ -672,6 +704,7 @@ public final class MediaSessionConnector {
int playbackState = player.getPlaybackState();
Bundle extras = new Bundle();
@Nullable
ExoPlaybackException playbackError =
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
boolean reportError = playbackError != null || customError != null;
@ -727,8 +760,8 @@ public final class MediaSessionConnector {
*
* @param commandReceiver The command receiver to register.
*/
public void registerCustomCommandReceiver(CommandReceiver commandReceiver) {
if (!customCommandReceivers.contains(commandReceiver)) {
public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
customCommandReceivers.add(commandReceiver);
}
}
@ -738,18 +771,22 @@ public final class MediaSessionConnector {
*
* @param commandReceiver The command receiver to unregister.
*/
public void unregisterCustomCommandReceiver(CommandReceiver commandReceiver) {
customCommandReceivers.remove(commandReceiver);
public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null) {
customCommandReceivers.remove(commandReceiver);
}
}
private void registerCommandReceiver(CommandReceiver commandReceiver) {
if (!commandReceivers.contains(commandReceiver)) {
private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
commandReceivers.add(commandReceiver);
}
}
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
commandReceivers.remove(commandReceiver);
private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null) {
commandReceivers.remove(commandReceiver);
}
}
private long buildPrepareActions() {
@ -807,39 +844,47 @@ public final class MediaSessionConnector {
}
}
@EnsuresNonNullIf(result = true, expression = "player")
private boolean canDispatchPlaybackAction(long action) {
return player != null && (enabledPlaybackActions & action) != 0;
}
@EnsuresNonNullIf(result = true, expression = "playbackPreparer")
private boolean canDispatchToPlaybackPreparer(long action) {
return playbackPreparer != null
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "queueNavigator"})
private boolean canDispatchToQueueNavigator(long action) {
return player != null
&& queueNavigator != null
&& (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "ratingCallback"})
private boolean canDispatchSetRating() {
return player != null && ratingCallback != null;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "queueEditor"})
private boolean canDispatchQueueEdit() {
return player != null && queueEditor != null;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "mediaButtonEventHandler"})
private boolean canDispatchMediaButtonEvent() {
return player != null && mediaButtonEventHandler != null;
}
private void setPlayWhenReady(boolean playWhenReady) {
if (player != null) {
controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady);
}
}
private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs);
@ -906,10 +951,10 @@ public final class MediaSessionConnector {
MediaSessionCompat.QueueItem queueItem = queue.get(i);
if (queueItem.getQueueId() == activeQueueItemId) {
MediaDescriptionCompat description = queueItem.getDescription();
Bundle extras = description.getExtras();
@Nullable Bundle extras = description.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
Object value = extras.get(key);
@Nullable Object value = extras.get(key);
if (value instanceof String) {
builder.putString(metadataExtrasPrefix + key, (String) value);
} else if (value instanceof CharSequence) {
@ -925,38 +970,40 @@ public final class MediaSessionConnector {
}
}
}
if (description.getTitle() != null) {
String title = String.valueOf(description.getTitle());
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
@Nullable CharSequence title = description.getTitle();
if (title != null) {
String titleString = String.valueOf(title);
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
}
if (description.getSubtitle() != null) {
@Nullable CharSequence subtitle = description.getSubtitle();
if (subtitle != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
String.valueOf(description.getSubtitle()));
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
}
if (description.getDescription() != null) {
@Nullable CharSequence displayDescription = description.getDescription();
if (displayDescription != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
String.valueOf(description.getDescription()));
String.valueOf(displayDescription));
}
if (description.getIconBitmap() != null) {
builder.putBitmap(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
@Nullable Bitmap iconBitmap = description.getIconBitmap();
if (iconBitmap != null) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
}
if (description.getIconUri() != null) {
@Nullable Uri iconUri = description.getIconUri();
if (iconUri != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
String.valueOf(description.getIconUri()));
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
}
if (description.getMediaId() != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_MEDIA_ID, description.getMediaId());
@Nullable String mediaId = description.getMediaId();
if (mediaId != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
}
if (description.getMediaUri() != null) {
@Nullable Uri mediaUri = description.getMediaUri();
if (mediaUri != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
String.valueOf(description.getMediaUri()));
MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
}
break;
}
@ -975,8 +1022,8 @@ public final class MediaSessionConnector {
// Player.EventListener implementation.
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
@ -1019,6 +1066,7 @@ public final class MediaSessionConnector {
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
@ -1045,19 +1093,20 @@ public final class MediaSessionConnector {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.onPrepare();
playbackPreparer.onPrepare(/* playWhenReady= */ true);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
setPlayWhenReady(/* playWhenReady= */ true);
controlDispatcher.dispatchSetPlayWhenReady(
Assertions.checkNotNull(player), /* playWhenReady= */ true);
}
}
@Override
public void onPause() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
setPlayWhenReady(/* playWhenReady= */ false);
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
}
@ -1180,56 +1229,49 @@ public final class MediaSessionConnector {
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
setPlayWhenReady(/* playWhenReady= */ false);
playbackPreparer.onPrepare();
playbackPreparer.onPrepare(/* playWhenReady= */ false);
}
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
setPlayWhenReady(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
setPlayWhenReady(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromSearch(query, extras);
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
setPlayWhenReady(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromUri(uri, extras);
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
setPlayWhenReady(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
setPlayWhenReady(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromSearch(query, extras);
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
setPlayWhenReady(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromUri(uri, extras);
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
}
}

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
@ -65,7 +66,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
@Override
public void onCustomAction(
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {

View file

@ -166,7 +166,7 @@ public final class TimelineQueueEditor
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
MediaSource mediaSource = sourceFactory.createMediaSource(description);
@Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) {
queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource);

View file

@ -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.mediasession;
import com.google.android.exoplayer2.util.NonNullApi;

View file

@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.12.1'
}

View file

@ -57,14 +57,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
private final @Nullable String userAgent;
private final @Nullable Predicate<String> contentTypePredicate;
private final @Nullable CacheControl cacheControl;
private final @Nullable RequestProperties defaultRequestProperties;
@Nullable private final String userAgent;
@Nullable private final CacheControl cacheControl;
@Nullable private final RequestProperties defaultRequestProperties;
private @Nullable DataSpec dataSpec;
private @Nullable Response response;
private @Nullable InputStream responseByteStream;
@Nullable private Predicate<String> contentTypePredicate;
@Nullable private DataSpec dataSpec;
@Nullable private Response response;
@Nullable private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
@ -79,7 +79,28 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param userAgent An optional User-Agent string.
*/
public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
this(callFactory, userAgent, /* contentTypePredicate= */ null);
this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent An optional User-Agent string.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
* server as HTTP headers on every request.
*/
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@Nullable CacheControl cacheControl,
@Nullable RequestProperties defaultRequestProperties) {
super(/* isNetwork= */ true);
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = userAgent;
this.cacheControl = cacheControl;
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
}
/**
@ -89,7 +110,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
* @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link
* #setContentTypePredicate(Predicate)}.
*/
@Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@ -110,9 +134,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
* the server as HTTP headers on every request.
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
* server as HTTP headers on every request.
* @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl,
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/
@Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@ -128,8 +155,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestProperties = new RequestProperties();
}
/**
* 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;
}
@Override
public @Nullable Uri getUri() {
@Nullable
public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}

View file

@ -29,9 +29,9 @@ import okhttp3.Call;
public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory;
private final @Nullable String userAgent;
private final @Nullable TransferListener listener;
private final @Nullable CacheControl cacheControl;
@Nullable private final String userAgent;
@Nullable private final TransferListener listener;
@Nullable private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
new OkHttpDataSource(
callFactory,
userAgent,
/* contentTypePredicate= */ null,
cacheControl,
defaultRequestProperties);
if (listener != null) {

View file

@ -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.okhttp;
import com.google.android.exoplayer2.util.NonNullApi;

Some files were not shown because too many files have changed in this diff Show more