1
.gitignore
vendored
|
|
@ -39,6 +39,7 @@ proguard-project.txt
|
|||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
dist
|
||||
tmp
|
||||
|
||||
|
|
|
|||
71
.hgignore
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Mercurial's .hgignore files can only be used in the root directory.
|
||||
# You can still apply these rules by adding
|
||||
# include:path/to/this/directory/.hgignore to the top-level .hgignore file.
|
||||
|
||||
# Ensure same syntax as in .gitignore can be used
|
||||
syntax:glob
|
||||
|
||||
# Android generated
|
||||
bin
|
||||
gen
|
||||
libs
|
||||
obj
|
||||
lint.xml
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
classes
|
||||
gen-external-apklibs
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
.checkstyle
|
||||
.cproject
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
build
|
||||
buildout
|
||||
out
|
||||
|
||||
# Maven
|
||||
target
|
||||
release.properties
|
||||
pom.xml.*
|
||||
|
||||
# Ant
|
||||
ant.properties
|
||||
local.properties
|
||||
proguard.cfg
|
||||
proguard-project.txt
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
dist
|
||||
tmp
|
||||
|
||||
# VP9 extension
|
||||
extensions/vp9/src/main/jni/libvpx
|
||||
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||
extensions/vp9/src/main/jni/libyuv
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
||||
# FLAC extension
|
||||
extensions/flac/src/main/jni/flac
|
||||
|
||||
# FFmpeg extension
|
||||
extensions/ffmpeg/src/main/jni/ffmpeg
|
||||
|
||||
# Cronet extension
|
||||
extensions/cronet/jniLibs/*
|
||||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!extensions/cronet/libs/README.md
|
||||
|
|
@ -69,7 +69,7 @@ individually.
|
|||
In addition to library modules, ExoPlayer has multiple extension modules that
|
||||
depend on external libraries to provide additional functionality. Some
|
||||
extensions are available from JCenter, whereas others must be built manaully.
|
||||
Browse the [extensions directory] and their individual READMEs for details.
|
||||
Browse the [extensions directory][] and their individual READMEs for details.
|
||||
|
||||
More information on the library and extension modules that are available from
|
||||
JCenter can be found on [Bintray][].
|
||||
|
|
|
|||
|
|
@ -1,5 +1,75 @@
|
|||
# Release notes #
|
||||
|
||||
### 2.6.0 ###
|
||||
|
||||
* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0".
|
||||
* New `Player.DefaultEventListener` abstract class can be extended to avoid
|
||||
having to implement all methods defined by `Player.EventListener`.
|
||||
* Added a reason to `EventListener.onPositionDiscontinuity`
|
||||
([#3252](https://github.com/google/ExoPlayer/issues/3252)).
|
||||
* New `setShuffleModeEnabled` method for enabling shuffled playback.
|
||||
* SimpleExoPlayer: Support for multiple video, text and metadata outputs.
|
||||
* Support for `Renderer`s that don't consume any media
|
||||
([#3212](https://github.com/google/ExoPlayer/issues/3212)).
|
||||
* Fix reporting of internal position discontinuities via
|
||||
`Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is
|
||||
added to disambiguate position adjustments during seeks from other types of
|
||||
internal position discontinuity.
|
||||
* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration`
|
||||
([#3362](https://github.com/google/ExoPlayer/issues/3362)).
|
||||
* Fix playbacks involving looping, concatenation and ads getting stuck when
|
||||
media contains tracks with uneven durations
|
||||
([#1874](https://github.com/google/ExoPlayer/issues/1874)).
|
||||
* Fix issue with `ContentDataSource` when reading from certain `ContentProvider`
|
||||
implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)).
|
||||
* Better playback experience when the video decoder cannot keep up, by skipping
|
||||
to key-frames. This is particularly relevant for variable speed playbacks.
|
||||
* Allow `SingleSampleMediaSource` to suppress load errors
|
||||
([#3140](https://github.com/google/ExoPlayer/issues/3140)).
|
||||
* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked
|
||||
after a dynamic playlist modification has been applied
|
||||
([#3407](https://github.com/google/ExoPlayer/issues/3407)).
|
||||
* Audio: New `AudioSink` interface allows customization of audio output path.
|
||||
* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming
|
||||
and progressive streams.
|
||||
* Track selection:
|
||||
* Fixed adaptive track selection logic for live playbacks
|
||||
([#3017](https://github.com/google/ExoPlayer/issues/3017)).
|
||||
* Added ability to select the lowest bitrate tracks.
|
||||
* DASH:
|
||||
* Don't crash when a malformed or unexpected manifest update occurs
|
||||
([#2795](https://github.com/google/ExoPlayer/issues/2795)).
|
||||
* HLS:
|
||||
* Support for Widevine protected FMP4 variants.
|
||||
* Support CEA-608 in FMP4 variants.
|
||||
* Support extractor injection
|
||||
([#2748](https://github.com/google/ExoPlayer/issues/2748)).
|
||||
* DRM:
|
||||
* Improved compatibility with ClearKey content
|
||||
([#3138](https://github.com/google/ExoPlayer/issues/3138)).
|
||||
* Support multiple PSSH boxes of the same type.
|
||||
* Retry initial provisioning and key requests if they fail
|
||||
* Fix incorrect parsing of non-CENC sinf boxes.
|
||||
* IMA extension:
|
||||
* Expose `AdsLoader` via getter
|
||||
([#3322](https://github.com/google/ExoPlayer/issues/3322)).
|
||||
* Handle `setPlayWhenReady` calls during ad playbacks
|
||||
([#3303](https://github.com/google/ExoPlayer/issues/3303)).
|
||||
* Ignore seeks if an ad is playing
|
||||
([#3309](https://github.com/google/ExoPlayer/issues/3309)).
|
||||
* Improve robustness of `ImaAdsLoader` in case content is not paused between
|
||||
content to ad transitions
|
||||
([#3430](https://github.com/google/ExoPlayer/issues/3430)).
|
||||
* UI:
|
||||
* Allow specifying a `Drawable` for the `TimeBar` scrubber
|
||||
([#3337](https://github.com/google/ExoPlayer/issues/3337)).
|
||||
* Allow multiple listeners on `TimeBar`
|
||||
([#3406](https://github.com/google/ExoPlayer/issues/3406)).
|
||||
* New Leanback extension: Simplifies binding Exoplayer to Leanback UI
|
||||
components.
|
||||
* Unit tests moved to Robolectric.
|
||||
* Misc bugfixes.
|
||||
|
||||
### r2.5.4 ###
|
||||
|
||||
* Remove unnecessary media playlist fetches during playback of live HLS streams.
|
||||
|
|
|
|||
10
build.gradle
|
|
@ -14,12 +14,10 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://maven.google.com"
|
||||
}
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.0-beta4'
|
||||
classpath 'com.android.tools.build:gradle:3.0.0'
|
||||
classpath 'com.novoda:bintray-release:0.5.0'
|
||||
}
|
||||
// Workaround for the following test coverage issue. Remove when fixed:
|
||||
|
|
@ -34,9 +32,7 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://maven.google.com"
|
||||
}
|
||||
google()
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = true
|
||||
|
|
|
|||
|
|
@ -12,19 +12,23 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
// by the library requires API level 16 or greater.
|
||||
minSdkVersion = 9
|
||||
compileSdkVersion = 25
|
||||
targetSdkVersion = 25
|
||||
buildToolsVersion = '25'
|
||||
minSdkVersion = 14
|
||||
compileSdkVersion = 26
|
||||
targetSdkVersion = 26
|
||||
buildToolsVersion = '26.0.2'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '25.4.0'
|
||||
supportLibraryVersion = '27.0.0'
|
||||
playServicesLibraryVersion = '11.4.2'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
releaseVersion = 'r2.5.4'
|
||||
junitVersion = '4.12'
|
||||
truthVersion = '0.35'
|
||||
robolectricVersion = '3.4.2'
|
||||
releaseVersion = '2.6.0'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ include modulePrefix + 'extension-okhttp'
|
|||
include modulePrefix + 'extension-opus'
|
||||
include modulePrefix + 'extension-vp9'
|
||||
include modulePrefix + 'extension-rtmp'
|
||||
include modulePrefix + 'extension-leanback'
|
||||
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
||||
|
|
@ -50,6 +51,7 @@ project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'exten
|
|||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
|
||||
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
||||
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
4
demos/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# ExoPlayer demos #
|
||||
|
||||
This directory contains applications that demonstrate how to use ExoPlayer.
|
||||
Browse the individual demos and their READMEs to learn more.
|
||||
4
demos/ima/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# IMA demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with the IMA SDK.
|
||||
47
demos/ima/build.gradle
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'extension-ima')
|
||||
}
|
||||
39
demos/ima/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo"
|
||||
android:versionCode="2600"
|
||||
android:versionName="2.6.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.imademo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
|
||||
/**
|
||||
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
|
||||
* {@link PlayerManager}, which this class instantiates.
|
||||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private SimpleExoPlayerView playerView;
|
||||
private PlayerManager player;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
playerView = findViewById(R.id.player_view);
|
||||
player = new PlayerManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
player.init(this, playerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
player.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
player.release();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.imademo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Manages the {@link ExoPlayer}, the IMA plugin and all video playback.
|
||||
*/
|
||||
/* package */ final class PlayerManager {
|
||||
|
||||
private final ImaAdsLoader adsLoader;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private long contentPosition;
|
||||
|
||||
public PlayerManager(Context context) {
|
||||
String adTag = context.getString(R.string.ad_tag_url);
|
||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||
}
|
||||
|
||||
public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
|
||||
// Create a default track selector.
|
||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||
|
||||
// Create a player instance.
|
||||
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||
|
||||
// Bind the player to the view.
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
|
||||
// Produces DataSource instances through which media data is loaded.
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
|
||||
Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
|
||||
// Produces Extractor instances for parsing the content media (i.e. not the ad).
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
|
||||
|
||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource = new ExtractorMediaSource(
|
||||
Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null);
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory,
|
||||
adsLoader, simpleExoPlayerView.getOverlayFrameLayout());
|
||||
|
||||
// Prepare the player with the source.
|
||||
player.seekTo(contentPosition);
|
||||
player.prepare(mediaSourceWithAds);
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
if (player != null) {
|
||||
contentPosition = player.getContentPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
adsLoader.release();
|
||||
}
|
||||
|
||||
}
|
||||
21
demos/ima/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"/>
|
||||
BIN
demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
24
demos/ima/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<string name="application_name">Exo IMA Demo</string>
|
||||
|
||||
<string name="content_url"><![CDATA[http://rmcdn.2mdn.net/MotifFiles/html/1248596/android_1330378998288.mp4]]></string>
|
||||
|
||||
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
<!-- 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.
|
||||
|
|
@ -13,12 +13,11 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="ExoMediaButton">
|
||||
<item name="android:background">?android:attr/selectableItemBackground</item>
|
||||
<item name="android:layout_width">@dimen/exo_media_button_width</item>
|
||||
<item name="android:layout_height">@dimen/exo_media_button_height</item>
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../constants.gradle'
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
|
|
@ -16,14 +16,14 @@
|
|||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2504"
|
||||
android:versionName="2.5.4">
|
||||
android:versionCode="2600"
|
||||
android:versionName="2.6.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
|
||||
<application
|
||||
android:label="@string/application_name"
|
||||
|
|
@ -29,7 +29,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
|||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
|
|
@ -55,10 +55,9 @@ import java.util.Locale;
|
|||
/**
|
||||
* Logs player events using {@link Log}.
|
||||
*/
|
||||
/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener,
|
||||
VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
|
||||
MetadataRenderer.Output {
|
||||
/* package */ final class EventLogger implements Player.EventListener, MetadataOutput,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final int MAX_TIMELINE_ITEM_LINES = 3;
|
||||
|
|
@ -101,8 +100,13 @@ import java.util.Locale;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
Log.d(TAG, "positionDiscontinuity");
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
Log.d(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
Log.d(TAG, "positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -205,7 +209,12 @@ import java.util.Locale;
|
|||
Log.d(TAG, "]");
|
||||
}
|
||||
|
||||
// MetadataRenderer.Output
|
||||
@Override
|
||||
public void onSeekProcessed() {
|
||||
Log.d(TAG, "seekProcessed");
|
||||
}
|
||||
|
||||
// MetadataOutput
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
|
|
@ -244,7 +253,7 @@ import java.util.Locale;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
|
||||
+ elapsedSinceLastFeedMs + "]", null);
|
||||
}
|
||||
|
|
@ -480,4 +489,19 @@ import java.util.Locale;
|
|||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
private static String getDiscontinuityReasonString(@Player.DiscontinuityReason int reason) {
|
||||
switch (reason) {
|
||||
case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
||||
return "PERIOD_TRANSITION";
|
||||
case Player.DISCONTINUITY_REASON_SEEK:
|
||||
return "SEEK";
|
||||
case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||
return "SEEK_ADJUSTMENT";
|
||||
case Player.DISCONTINUITY_REASON_INTERNAL:
|
||||
return "INTERNAL";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,14 +34,12 @@ import android.widget.LinearLayout;
|
|||
import android.widget.TextView;
|
||||
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.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.EventListener;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
|
|
@ -56,6 +54,8 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
|||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
|
|
@ -73,8 +73,6 @@ import com.google.android.exoplayer2.upstream.DataSource;
|
|||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
|
|
@ -83,12 +81,13 @@ import java.util.UUID;
|
|||
/**
|
||||
* An activity that plays media using {@link SimpleExoPlayer}.
|
||||
*/
|
||||
public class PlayerActivity extends Activity implements OnClickListener, EventListener,
|
||||
public class PlayerActivity extends Activity implements OnClickListener,
|
||||
PlaybackControlView.VisibilityListener {
|
||||
|
||||
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
public static final String DRM_LICENSE_URL = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION = "drm_multi_session";
|
||||
public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
|
|
@ -128,9 +127,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
|
||||
// Fields used only for ad playback. The ads loader is loaded via reflection.
|
||||
|
||||
private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader
|
||||
private AdsLoader adsLoader;
|
||||
private Uri loadedAdTagUri;
|
||||
private ViewGroup adOverlayViewGroup;
|
||||
private ViewGroup adUiViewGroup;
|
||||
|
||||
// Activity lifecycle
|
||||
|
||||
|
|
@ -148,12 +147,12 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
setContentView(R.layout.player_activity);
|
||||
View rootView = findViewById(R.id.root);
|
||||
rootView.setOnClickListener(this);
|
||||
debugRootView = (LinearLayout) findViewById(R.id.controls_root);
|
||||
debugTextView = (TextView) findViewById(R.id.debug_text_view);
|
||||
retryButton = (Button) findViewById(R.id.retry_button);
|
||||
debugRootView = findViewById(R.id.controls_root);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
retryButton = findViewById(R.id.retry_button);
|
||||
retryButton.setOnClickListener(this);
|
||||
|
||||
simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
|
||||
simpleExoPlayerView = findViewById(R.id.player_view);
|
||||
simpleExoPlayerView.setControllerVisibilityListener(this);
|
||||
simpleExoPlayerView.requestFocus();
|
||||
}
|
||||
|
|
@ -177,7 +176,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if ((Util.SDK_INT <= 23 || player == null)) {
|
||||
if (Util.SDK_INT <= 23 || player == null) {
|
||||
initializePlayer();
|
||||
}
|
||||
}
|
||||
|
|
@ -219,8 +218,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// If the event was not handled then see if the player view can handle it.
|
||||
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event);
|
||||
// See whether the player view wants to handle media or DPAD keys events.
|
||||
return simpleExoPlayerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// OnClickListener methods
|
||||
|
|
@ -264,13 +263,14 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
if (drmSchemeUuid != null) {
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
|
||||
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
|
||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
|
||||
int errorStringId = R.string.error_drm_unknown;
|
||||
if (Util.SDK_INT < 18) {
|
||||
errorStringId = R.string.error_drm_not_supported;
|
||||
} else {
|
||||
try {
|
||||
drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
|
||||
keyRequestPropertiesArray);
|
||||
keyRequestPropertiesArray, multiSession);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
|
||||
|
|
@ -292,7 +292,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
drmSessionManager, extensionRendererMode);
|
||||
|
||||
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
player.addListener(this);
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.addListener(eventLogger);
|
||||
player.addMetadataOutput(eventLogger);
|
||||
player.setAudioDebugListener(eventLogger);
|
||||
|
|
@ -358,7 +358,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
|
||||
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
|
||||
@ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
|
||||
: Util.inferContentType("." + overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
|
|
@ -379,7 +379,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
}
|
||||
|
||||
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(UUID uuid,
|
||||
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
|
||||
String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
throws UnsupportedDrmException {
|
||||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
|
||||
buildHttpDataSourceFactory(false));
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
|
|
@ -389,7 +390,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
}
|
||||
}
|
||||
return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
|
||||
null, mainHandler, eventLogger);
|
||||
null, mainHandler, eventLogger, multiSession);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
|
|
@ -450,136 +451,25 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
// 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.
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
if (imaAdsLoader == null) {
|
||||
imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class)
|
||||
if (adsLoader == null) {
|
||||
adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class)
|
||||
.newInstance(this, adTagUri);
|
||||
adOverlayViewGroup = new FrameLayout(this);
|
||||
adUiViewGroup = new FrameLayout(this);
|
||||
// The demo app has a non-null overlay frame layout.
|
||||
simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup);
|
||||
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
||||
}
|
||||
Class<?> sourceClass =
|
||||
Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource");
|
||||
Constructor<?> constructor = sourceClass.getConstructor(MediaSource.class,
|
||||
DataSource.Factory.class, loaderClass, ViewGroup.class);
|
||||
return (MediaSource) constructor.newInstance(mediaSource, mediaDataSourceFactory, imaAdsLoader,
|
||||
adOverlayViewGroup);
|
||||
return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup);
|
||||
}
|
||||
|
||||
private void releaseAdsLoader() {
|
||||
if (imaAdsLoader != null) {
|
||||
try {
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
Method releaseMethod = loaderClass.getMethod("release");
|
||||
releaseMethod.invoke(imaAdsLoader);
|
||||
} catch (Exception e) {
|
||||
// Should never happen.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
imaAdsLoader = null;
|
||||
if (adsLoader != null) {
|
||||
adsLoader.release();
|
||||
adsLoader = null;
|
||||
loadedAdTagUri = null;
|
||||
simpleExoPlayerView.getOverlayFrameLayout().removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
// Player.EventListener implementation
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
if (inErrorState) {
|
||||
// This will only occur if the user has performed a seek whilst in the error state. Update the
|
||||
// resume position so that if the user then retries, playback will resume from the position to
|
||||
// which they seeked.
|
||||
updateResumePosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
String errorString = null;
|
||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||
Exception cause = e.getRendererException();
|
||||
if (cause instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) cause;
|
||||
if (decoderInitializationException.decoderName == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString = getString(R.string.error_no_secure_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString = getString(R.string.error_no_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
errorString = getString(R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.decoderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorString != null) {
|
||||
showToast(errorString);
|
||||
}
|
||||
inErrorState = true;
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearResumePosition();
|
||||
initializePlayer();
|
||||
} else {
|
||||
updateResumePosition();
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
updateButtonVisibilities();
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
}
|
||||
}
|
||||
|
||||
// User controls
|
||||
|
||||
private void updateButtonVisibilities() {
|
||||
|
|
@ -649,4 +539,85 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
|||
return false;
|
||||
}
|
||||
|
||||
private class PlayerEventListener extends Player.DefaultEventListener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (inErrorState) {
|
||||
// This will only occur if the user has performed a seek whilst in the error state. Update
|
||||
// the resume position so that if the user then retries, playback will resume from the
|
||||
// position to which they seeked.
|
||||
updateResumePosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
String errorString = null;
|
||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||
Exception cause = e.getRendererException();
|
||||
if (cause instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) cause;
|
||||
if (decoderInitializationException.decoderName == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString = getString(R.string.error_no_secure_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString = getString(R.string.error_no_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
errorString = getString(R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.decoderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorString != null) {
|
||||
showToast(errorString);
|
||||
}
|
||||
inErrorState = true;
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearResumePosition();
|
||||
initializePlayer();
|
||||
} else {
|
||||
updateResumePosition();
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
updateButtonVisibilities();
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ public class SampleChooserActivity extends Activity {
|
|||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list);
|
||||
ExpandableListView sampleList = findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleAdapter(this, groups));
|
||||
sampleList.setOnChildClickListener(new OnChildClickListener() {
|
||||
@Override
|
||||
|
|
@ -182,6 +182,7 @@ public class SampleChooserActivity extends Activity {
|
|||
UUID drmUuid = null;
|
||||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
boolean drmMultiSession = false;
|
||||
boolean preferExtensionDecoders = false;
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
|
|
@ -220,6 +221,9 @@ public class SampleChooserActivity extends Activity {
|
|||
reader.endObject();
|
||||
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
|
||||
break;
|
||||
case "drm_multi_session":
|
||||
drmMultiSession = reader.nextBoolean();
|
||||
break;
|
||||
case "prefer_extension_decoders":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
"Invalid attribute on nested item: prefer_extension_decoders");
|
||||
|
|
@ -242,15 +246,16 @@ public class SampleChooserActivity extends Activity {
|
|||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl,
|
||||
drmKeyRequestProperties, drmMultiSession);
|
||||
if (playlistSamples != null) {
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
||||
new UriSample[playlistSamples.size()]);
|
||||
return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
|
||||
preferExtensionDecoders, playlistSamplesArray);
|
||||
return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo,
|
||||
playlistSamplesArray);
|
||||
} else {
|
||||
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
|
||||
preferExtensionDecoders, uri, extension, adTagUri);
|
||||
return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension,
|
||||
adTagUri);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +276,7 @@ public class SampleChooserActivity extends Activity {
|
|||
return C.WIDEVINE_UUID;
|
||||
case "playready":
|
||||
return C.PLAYREADY_UUID;
|
||||
case "cenc":
|
||||
case "clearkey":
|
||||
return C.CLEARKEY_UUID;
|
||||
default:
|
||||
try {
|
||||
|
|
@ -372,31 +377,47 @@ public class SampleChooserActivity extends Activity {
|
|||
|
||||
}
|
||||
|
||||
private abstract static class Sample {
|
||||
|
||||
public final String name;
|
||||
public final boolean preferExtensionDecoders;
|
||||
private static final class DrmInfo {
|
||||
public final UUID drmSchemeUuid;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean preferExtensionDecoders) {
|
||||
this.name = name;
|
||||
public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean drmMultiSession) {
|
||||
this.drmSchemeUuid = drmSchemeUuid;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
}
|
||||
|
||||
public void updateIntent(Intent intent) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
|
||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class Sample {
|
||||
public final String name;
|
||||
public final boolean preferExtensionDecoders;
|
||||
public final DrmInfo drmInfo;
|
||||
|
||||
public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) {
|
||||
this.name = name;
|
||||
this.preferExtensionDecoders = preferExtensionDecoders;
|
||||
this.drmInfo = drmInfo;
|
||||
}
|
||||
|
||||
public Intent buildIntent(Context context) {
|
||||
Intent intent = new Intent(context, PlayerActivity.class);
|
||||
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
|
||||
if (drmSchemeUuid != null) {
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
|
||||
if (drmInfo != null) {
|
||||
drmInfo.updateIntent(intent);
|
||||
}
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
|
@ -408,10 +429,9 @@ public class SampleChooserActivity extends Activity {
|
|||
public final String extension;
|
||||
public final String adTagUri;
|
||||
|
||||
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
|
||||
public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri,
|
||||
String extension, String adTagUri) {
|
||||
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
|
||||
super(name, preferExtensionDecoders, drmInfo);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.adTagUri = adTagUri;
|
||||
|
|
@ -432,10 +452,9 @@ public class SampleChooserActivity extends Activity {
|
|||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean preferExtensionDecoders,
|
||||
public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo,
|
||||
UriSample... children) {
|
||||
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
|
||||
super(name, preferExtensionDecoders, drmInfo);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ import java.util.Arrays;
|
|||
private View buildView(Context context) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
View view = inflater.inflate(R.layout.track_selection_dialog, null);
|
||||
ViewGroup root = (ViewGroup) view.findViewById(R.id.root);
|
||||
ViewGroup root = view.findViewById(R.id.root);
|
||||
|
||||
TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
|
||||
new int[] {android.R.attr.selectableItemBackground});
|
||||
BIN
demos/main/src/main/res/drawable-xhdpi/ic_banner.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
demos/main/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/main/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -13,7 +13,6 @@
|
|||
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</string>
|
||||
|
|
@ -13,7 +13,6 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
|
|
@ -27,6 +27,11 @@ android {
|
|||
sourceSets.main {
|
||||
jniLibs.srcDirs = ['jniLibs']
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
|
@ -22,11 +21,8 @@ import static org.mockito.Mockito.times;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -68,7 +64,6 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.GINGERBREAD)
|
||||
@Test
|
||||
public void testReadPartialBuffer() throws IOException {
|
||||
byte[] firstHalf = Arrays.copyOfRange(TEST_DATA, 0, TEST_DATA.length / 2);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
|
@ -75,12 +74,13 @@ import org.mockito.stubbing.Answer;
|
|||
public final class CronetDataSourceTest {
|
||||
|
||||
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
||||
private static final int TEST_READ_TIMEOUT_MS = 50;
|
||||
private static final int TEST_READ_TIMEOUT_MS = 100;
|
||||
private static final String TEST_URL = "http://google.com";
|
||||
private static final String TEST_CONTENT_TYPE = "test/test";
|
||||
private static final byte[] TEST_POST_BODY = "test post body".getBytes();
|
||||
private static final long TEST_CONTENT_LENGTH = 16000L;
|
||||
private static final int TEST_CONNECTION_STATUS = 5;
|
||||
private static final int TEST_INVALID_CONNECTION_STATUS = -1;
|
||||
|
||||
private DataSpec testDataSpec;
|
||||
private DataSpec testPostDataSpec;
|
||||
|
|
@ -103,6 +103,7 @@ public final class CronetDataSourceTest {
|
|||
@Mock private CronetEngine mockCronetEngine;
|
||||
|
||||
private CronetDataSource dataSourceUnderTest;
|
||||
private boolean redirectCalled;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
|
@ -119,10 +120,11 @@ public final class CronetDataSourceTest {
|
|||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null));
|
||||
null,
|
||||
false));
|
||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||
when(mockCronetEngine.newUrlRequestBuilder(
|
||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||
.thenReturn(mockUrlRequestBuilder);
|
||||
when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
|
||||
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
||||
|
|
@ -139,10 +141,14 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
private UrlResponseInfo createUrlResponseInfo(int statusCode) {
|
||||
return createUrlResponseInfoWithUrl(TEST_URL, statusCode);
|
||||
}
|
||||
|
||||
private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) {
|
||||
ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
|
||||
responseHeaderList.addAll(testResponseHeader.entrySet());
|
||||
return new UrlResponseInfoImpl(
|
||||
Collections.singletonList(TEST_URL),
|
||||
Collections.singletonList(url),
|
||||
statusCode,
|
||||
null, // httpStatusText
|
||||
responseHeaderList,
|
||||
|
|
@ -151,11 +157,16 @@ public final class CronetDataSourceTest {
|
|||
null); // proxyServer
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
@Test
|
||||
public void testOpeningTwiceThrows() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail("Expected IllegalStateException.");
|
||||
} catch (IllegalStateException e) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -564,6 +575,45 @@ public final class CronetDataSourceTest {
|
|||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectInterrupted() {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final ConditionVariable timedOutCondition = new ConditionVariable();
|
||||
|
||||
Thread thread =
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertTrue(e instanceof CronetDataSource.OpenException);
|
||||
assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
|
||||
assertEquals(
|
||||
TEST_INVALID_CONNECTION_STATUS,
|
||||
((CronetDataSource.OpenException) e).cronetConnectionStatus);
|
||||
timedOutCondition.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
startCondition.block();
|
||||
|
||||
// We should still be trying to open.
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutCondition.block();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectResponseBeforeTimeout() {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
|
|
@ -650,6 +700,111 @@ public final class CronetDataSourceTest {
|
|||
assertEquals(1, openExceptions.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
|
||||
throws HttpDataSourceException {
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
|
||||
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
|
||||
verify(mockUrlRequest).followRedirect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
true));
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
|
||||
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class));
|
||||
verify(mockUrlRequestBuilder, never()).addHeader(eq("Range"), any(String.class));
|
||||
verify(mockUrlRequestBuilder, times(2)).addHeader("Content-Type", TEST_CONTENT_TYPE);
|
||||
verify(mockUrlRequest, never()).followRedirect();
|
||||
verify(mockUrlRequest, times(2)).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
||||
throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
true));
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
|
||||
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class));
|
||||
verify(mockUrlRequestBuilder, times(2)).addHeader("Range", "bytes=1000-5999");
|
||||
verify(mockUrlRequestBuilder, times(2)).addHeader("Content-Type", TEST_CONTENT_TYPE);
|
||||
verify(mockUrlRequest, never()).followRedirect();
|
||||
verify(mockUrlRequest, times(2)).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
|
||||
verify(mockUrlRequest).followRedirect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
true));
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
|
||||
verify(mockUrlRequest).followRedirect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionFromTransferListener() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
|
@ -684,6 +839,38 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadInterrupted() throws HttpDataSourceException {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
final ConditionVariable startCondition = buildReadStartedCondition();
|
||||
final ConditionVariable timedOutCondition = new ConditionVariable();
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
Thread thread =
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
|
||||
timedOutCondition.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
startCondition.block();
|
||||
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutCondition.block();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowDirectExecutor() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
|
|
@ -732,6 +919,38 @@ public final class CronetDataSourceTest {
|
|||
}).when(mockUrlRequest).start();
|
||||
}
|
||||
|
||||
private void mockSingleRedirectSuccess() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (!redirectCalled) {
|
||||
redirectCalled = true;
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
||||
"http://example.com/video/redirect");
|
||||
} else {
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
}
|
||||
|
||||
private void mockFollowRedirectSuccess() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).followRedirect();
|
||||
}
|
||||
|
||||
private void mockResponseStartFailure() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
|
|
@ -769,16 +988,34 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
private void mockReadFailure() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.read(any(ByteBuffer.class));
|
||||
}
|
||||
|
||||
private ConditionVariable buildReadStartedCondition() {
|
||||
final ConditionVariable startedCondition = new ConditionVariable();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
startedCondition.open();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.read(any(ByteBuffer.class));
|
||||
return startedCondition;
|
||||
}
|
||||
|
||||
private ConditionVariable buildUrlRequestStartedCondition() {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.ConditionVariable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
|
@ -27,6 +26,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
|
|||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
|
@ -74,6 +74,14 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
}
|
||||
|
||||
/** Thrown on catching an InterruptedException. */
|
||||
public static final class InterruptedIOException extends IOException {
|
||||
|
||||
public InterruptedIOException(InterruptedException e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
|
||||
}
|
||||
|
|
@ -89,6 +97,9 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
private static final String TAG = "CronetDataSource";
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
private static final String SET_COOKIE = "Set-Cookie";
|
||||
private static final String COOKIE = "Cookie";
|
||||
|
||||
private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
|
||||
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
|
||||
// The size of read buffer passed to cronet UrlRequest.read().
|
||||
|
|
@ -101,6 +112,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
private final boolean handleSetCookieRequests;
|
||||
private final RequestProperties defaultRequestProperties;
|
||||
private final RequestProperties requestProperties;
|
||||
private final ConditionVariable operation;
|
||||
|
|
@ -144,7 +156,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null);
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,13 +180,40 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties);
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, 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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
* @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 handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||
* the redirect url in the "Cookie" header.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties,
|
||||
handleSetCookieRequests);
|
||||
}
|
||||
|
||||
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
||||
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
||||
this.executor = Assertions.checkNotNull(executor);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
|
|
@ -184,6 +223,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
this.clock = Assertions.checkNotNull(clock);
|
||||
this.defaultRequestProperties = defaultRequestProperties;
|
||||
this.handleSetCookieRequests = handleSetCookieRequests;
|
||||
requestProperties = new RequestProperties();
|
||||
operation = new ConditionVariable();
|
||||
}
|
||||
|
|
@ -223,15 +263,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
operation.close();
|
||||
resetConnectTimeout();
|
||||
currentDataSpec = dataSpec;
|
||||
currentUrlRequest = buildRequest(dataSpec);
|
||||
try {
|
||||
currentUrlRequest = buildRequestBuilder(dataSpec).build();
|
||||
} catch (IOException e) {
|
||||
throw new OpenException(e, currentDataSpec, Status.IDLE);
|
||||
}
|
||||
currentUrlRequest.start();
|
||||
boolean requestStarted = blockUntilConnectTimeout();
|
||||
|
||||
if (exception != null) {
|
||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
||||
} else if (!requestStarted) {
|
||||
// The timeout was reached before the connection was opened.
|
||||
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
try {
|
||||
boolean connectionOpened = blockUntilConnectTimeout();
|
||||
if (exception != null) {
|
||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
||||
} else if (!connectionOpened) {
|
||||
// The timeout was reached before the connection was opened.
|
||||
throw new OpenException(
|
||||
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
|
||||
}
|
||||
|
||||
// Check for a valid response code.
|
||||
|
|
@ -299,14 +348,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
operation.close();
|
||||
readBuffer.clear();
|
||||
currentUrlRequest.read(readBuffer);
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
// We're timing out, but since the operation is still ongoing we'll need to replace
|
||||
// readBuffer to avoid the possibility of it being written to by this operation during a
|
||||
// subsequent request.
|
||||
try {
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
} catch (InterruptedException | SocketTimeoutException e) {
|
||||
// If we're timing out or getting interrupted, the operation is still ongoing.
|
||||
// So we'll need to replace readBuffer to avoid the possibility of it being written to by
|
||||
// this operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
throw new HttpDataSourceException(
|
||||
new SocketTimeoutException(), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
} else if (exception != null) {
|
||||
e instanceof InterruptedException
|
||||
? new InterruptedIOException((InterruptedException) e)
|
||||
: (SocketTimeoutException) e,
|
||||
currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new HttpDataSourceException(exception, currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
} else if (finished) {
|
||||
|
|
@ -379,7 +438,28 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
if (resetTimeoutOnRedirects) {
|
||||
resetConnectTimeout();
|
||||
}
|
||||
request.followRedirect();
|
||||
|
||||
Map<String, List<String>> headers = info.getAllHeaders();
|
||||
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
|
||||
request.followRedirect();
|
||||
} else {
|
||||
currentUrlRequest.cancel();
|
||||
DataSpec redirectUrlDataSpec = new DataSpec(Uri.parse(newLocationUrl),
|
||||
currentDataSpec.postBody, currentDataSpec.absoluteStreamPosition,
|
||||
currentDataSpec.position, currentDataSpec.length, currentDataSpec.key,
|
||||
currentDataSpec.flags);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -427,7 +507,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
// Internal methods.
|
||||
|
||||
private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
|
||||
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
||||
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
|
||||
dataSpec.uri.toString(), this, executor).allowDirectExecutor();
|
||||
// Set the headers.
|
||||
|
|
@ -446,20 +526,25 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
|
||||
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
|
||||
Status.IDLE);
|
||||
throw new IOException("POST request with non-empty body must set Content-Type");
|
||||
}
|
||||
// Set the Range header.
|
||||
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
|
||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||
StringBuilder rangeValue = new StringBuilder();
|
||||
rangeValue.append("bytes=");
|
||||
rangeValue.append(currentDataSpec.position);
|
||||
rangeValue.append(dataSpec.position);
|
||||
rangeValue.append("-");
|
||||
if (currentDataSpec.length != C.LENGTH_UNSET) {
|
||||
rangeValue.append(currentDataSpec.position + currentDataSpec.length - 1);
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
rangeValue.append(dataSpec.position + dataSpec.length - 1);
|
||||
}
|
||||
requestBuilder.addHeader("Range", rangeValue.toString());
|
||||
}
|
||||
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
|
||||
// (adjusting the code as necessary).
|
||||
// Force identity encoding unless gzip is allowed.
|
||||
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
||||
// requestBuilder.addHeader("Accept-Encoding", "identity");
|
||||
// }
|
||||
// Set the method and (if non-empty) the body.
|
||||
if (dataSpec.postBody != null) {
|
||||
requestBuilder.setHttpMethod("POST");
|
||||
|
|
@ -468,10 +553,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
executor);
|
||||
}
|
||||
}
|
||||
return requestBuilder.build();
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
private boolean blockUntilConnectTimeout() {
|
||||
private boolean blockUntilConnectTimeout() throws InterruptedException {
|
||||
long now = clock.elapsedRealtime();
|
||||
boolean opened = false;
|
||||
while (!opened && now < currentConnectTimeoutMs) {
|
||||
|
|
@ -538,7 +623,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
return contentLength;
|
||||
}
|
||||
|
||||
private static int getStatus(UrlRequest request) {
|
||||
private static String parseCookies(List<String> setCookieHeaders) {
|
||||
return TextUtils.join(";", setCookieHeaders);
|
||||
}
|
||||
|
||||
private static void attachCookies(UrlRequest.Builder requestBuilder, String cookies) {
|
||||
if (TextUtils.isEmpty(cookies)) {
|
||||
return;
|
||||
}
|
||||
requestBuilder.addHeader(COOKIE, cookies);
|
||||
}
|
||||
|
||||
private static int getStatus(UrlRequest request) throws InterruptedException {
|
||||
final ConditionVariable conditionVariable = new ConditionVariable();
|
||||
final int[] statusHolder = new int[1];
|
||||
request.getStatus(new UrlRequest.StatusListener() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for
|
||||
decoding and can render audio encoded in a variety of formats.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
|
|
@ -58,13 +59,18 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
String sampleMimeType = format.sampleMimeType;
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
}
|
||||
String mimeType = format.sampleMimeType;
|
||||
return FfmpegLibrary.supportsFormat(mimeType) ? FORMAT_HANDLED
|
||||
: MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which
|
||||
use libFLAC (the Flac decoding library) to extract and decode FLAC audio.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
|
|
@ -30,7 +38,7 @@ NDK_PATH="<path to Android NDK>"
|
|||
|
||||
```
|
||||
cd "${FLAC_EXT_PATH}/jni" && \
|
||||
curl http://downloads.xiph.org/releases/flac/flac-1.3.1.tar.xz | tar xJ && \
|
||||
curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.1.tar.xz | tar xJ && \
|
||||
mv flac-1.3.1 flac
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.flac.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
|
|
|||
|
|
@ -22,15 +22,11 @@ import android.test.InstrumentationTestCase;
|
|||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
/**
|
||||
|
|
@ -45,20 +41,22 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
private void playUri(String uri) throws ExoPlaybackException {
|
||||
TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri),
|
||||
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
|
||||
getInstrumentation().getContext());
|
||||
Thread thread = new Thread(testPlaybackRunnable);
|
||||
thread.start();
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
fail(); // Should never happen.
|
||||
}
|
||||
if (thread.playbackException != null) {
|
||||
throw thread.playbackException;
|
||||
if (testPlaybackRunnable.playbackException != null) {
|
||||
throw testPlaybackRunnable.playbackException;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestPlaybackThread extends Thread implements Player.EventListener {
|
||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
||||
implements Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
|
@ -66,7 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
private ExoPlayer player;
|
||||
private ExoPlaybackException playbackException;
|
||||
|
||||
public TestPlaybackThread(Uri uri, Context context) {
|
||||
public TestPlaybackRunnable(Uri uri, Context context) {
|
||||
this.uri = uri;
|
||||
this.context = context;
|
||||
}
|
||||
|
|
@ -89,31 +87,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
Looper.loop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
playbackException = error;
|
||||
|
|
@ -123,20 +96,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED
|
||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||
releasePlayerAndQuitLooper();
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
|
|
@ -122,10 +124,20 @@ public final class FlacExtractor implements Extractor {
|
|||
}
|
||||
|
||||
});
|
||||
|
||||
Format mediaFormat = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null,
|
||||
streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate,
|
||||
C.ENCODING_PCM_16BIT, null, null, 0, null);
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
null,
|
||||
streamInfo.bitRate(),
|
||||
Format.NO_VALUE,
|
||||
streamInfo.channels,
|
||||
streamInfo.sampleRate,
|
||||
getPcmEncoding(streamInfo.bitsPerSample),
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null);
|
||||
trackOutput.format(mediaFormat);
|
||||
|
||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
|
|
@ -46,9 +47,16 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
|
||||
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
if (!FlacLibrary.isAvailable()
|
||||
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@
|
|||
#define CHECK(x) \
|
||||
if (!(x)) ALOGE("Check failed: %s ", #x)
|
||||
|
||||
const int endian = 1;
|
||||
#define isBigEndian() (*(reinterpret_cast<const char *>(&endian)) == 0)
|
||||
|
||||
// The FLAC parser calls our C++ static callbacks using C calling conventions,
|
||||
// inside FLAC__stream_decoder_process_until_end_of_metadata
|
||||
// and FLAC__stream_decoder_process_single.
|
||||
|
|
@ -180,85 +183,42 @@ void FLACParser::errorCallback(FLAC__StreamDecoderErrorStatus status) {
|
|||
mErrorStatus = status;
|
||||
}
|
||||
|
||||
// Copy samples from FLAC native 32-bit non-interleaved to 16-bit interleaved.
|
||||
// Copy samples from FLAC native 32-bit non-interleaved to
|
||||
// correct bit-depth (non-zero padded), interleaved.
|
||||
// These are candidates for optimization if needed.
|
||||
|
||||
static void copyMono8(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned /* nChannels */) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
*dst++ = src[0][i] << 8;
|
||||
}
|
||||
}
|
||||
|
||||
static void copyStereo8(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned /* nChannels */) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
*dst++ = src[0][i] << 8;
|
||||
*dst++ = src[1][i] << 8;
|
||||
}
|
||||
}
|
||||
|
||||
static void copyMultiCh8(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned nChannels) {
|
||||
static void copyToByteArrayBigEndian(int8_t *dst, const int *const *src,
|
||||
unsigned bytesPerSample, unsigned nSamples,
|
||||
unsigned nChannels) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
for (unsigned c = 0; c < nChannels; ++c) {
|
||||
*dst++ = src[c][i] << 8;
|
||||
// point to the first byte of the source address
|
||||
// and then skip the first few bytes (most significant bytes)
|
||||
// depending on the bit depth
|
||||
const int8_t *byteSrc =
|
||||
reinterpret_cast<const int8_t *>(&src[c][i]) + 4 - bytesPerSample;
|
||||
memcpy(dst, byteSrc, bytesPerSample);
|
||||
dst = dst + bytesPerSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void copyMono16(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned /* nChannels */) {
|
||||
static void copyToByteArrayLittleEndian(int8_t *dst, const int *const *src,
|
||||
unsigned bytesPerSample,
|
||||
unsigned nSamples, unsigned nChannels) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
*dst++ = src[0][i];
|
||||
for (unsigned c = 0; c < nChannels; ++c) {
|
||||
// with little endian, the most significant bytes will be at the end
|
||||
// copy the bytes in little endian will remove the most significant byte
|
||||
// so we are good here.
|
||||
memcpy(dst, &(src[c][i]), bytesPerSample);
|
||||
dst = dst + bytesPerSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void copyStereo16(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
static void copyTrespass(int8_t * /* dst */, const int *const * /* src */,
|
||||
unsigned /* bytesPerSample */, unsigned /* nSamples */,
|
||||
unsigned /* nChannels */) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
*dst++ = src[0][i];
|
||||
*dst++ = src[1][i];
|
||||
}
|
||||
}
|
||||
|
||||
static void copyMultiCh16(int16_t *dst, const int *const *src,
|
||||
unsigned nSamples, unsigned nChannels) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
for (unsigned c = 0; c < nChannels; ++c) {
|
||||
*dst++ = src[c][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 24-bit versions should do dithering or noise-shaping, here or in AudioFlinger
|
||||
|
||||
static void copyMono24(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned /* nChannels */) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
*dst++ = src[0][i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static void copyStereo24(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned /* nChannels */) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
*dst++ = src[0][i] >> 8;
|
||||
*dst++ = src[1][i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static void copyMultiCh24(int16_t *dst, const int *const *src,
|
||||
unsigned nSamples, unsigned nChannels) {
|
||||
for (unsigned i = 0; i < nSamples; ++i) {
|
||||
for (unsigned c = 0; c < nChannels; ++c) {
|
||||
*dst++ = src[c][i] >> 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void copyTrespass(int16_t * /* dst */, const int *const * /* src */,
|
||||
unsigned /* nSamples */, unsigned /* nChannels */) {
|
||||
TRESPASS();
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +300,7 @@ bool FLACParser::decodeMetadata() {
|
|||
case 8:
|
||||
case 16:
|
||||
case 24:
|
||||
case 32:
|
||||
break;
|
||||
default:
|
||||
ALOGE("unsupported bits per sample %u", getBitsPerSample());
|
||||
|
|
@ -363,23 +324,11 @@ bool FLACParser::decodeMetadata() {
|
|||
ALOGE("unsupported sample rate %u", getSampleRate());
|
||||
return false;
|
||||
}
|
||||
// configure the appropriate copy function, defaulting to trespass
|
||||
static const struct {
|
||||
unsigned mChannels;
|
||||
unsigned mBitsPerSample;
|
||||
void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned nChannels);
|
||||
} table[] = {
|
||||
{1, 8, copyMono8}, {2, 8, copyStereo8}, {8, 8, copyMultiCh8},
|
||||
{1, 16, copyMono16}, {2, 16, copyStereo16}, {8, 16, copyMultiCh16},
|
||||
{1, 24, copyMono24}, {2, 24, copyStereo24}, {8, 24, copyMultiCh24},
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof(table) / sizeof(table[0]); ++i) {
|
||||
if (table[i].mChannels >= getChannels() &&
|
||||
table[i].mBitsPerSample == getBitsPerSample()) {
|
||||
mCopy = table[i].mCopy;
|
||||
break;
|
||||
}
|
||||
// configure the appropriate copy function based on device endianness.
|
||||
if (isBigEndian()) {
|
||||
mCopy = copyToByteArrayBigEndian;
|
||||
} else {
|
||||
mCopy = copyToByteArrayLittleEndian;
|
||||
}
|
||||
} else {
|
||||
ALOGE("missing STREAMINFO");
|
||||
|
|
@ -424,7 +373,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
size_t bufferSize = blocksize * getChannels() * sizeof(int16_t);
|
||||
unsigned bytesPerSample = getBitsPerSample() >> 3;
|
||||
size_t bufferSize = blocksize * getChannels() * bytesPerSample;
|
||||
if (bufferSize > output_size) {
|
||||
ALOGE(
|
||||
"FLACParser::readBuffer not enough space in output buffer "
|
||||
|
|
@ -434,8 +384,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) {
|
|||
}
|
||||
|
||||
// copy PCM from FLAC write buffer to our media buffer, with interleaving.
|
||||
(*mCopy)(reinterpret_cast<int16_t *>(output), mWriteBuffer, blocksize,
|
||||
getChannels());
|
||||
(*mCopy)(reinterpret_cast<int8_t *>(output), mWriteBuffer, bytesPerSample,
|
||||
blocksize, getChannels());
|
||||
|
||||
// fill in buffer metadata
|
||||
CHECK(mWriteHeader.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER);
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ class FLACParser {
|
|||
private:
|
||||
DataSource *mDataSource;
|
||||
|
||||
void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples,
|
||||
unsigned nChannels);
|
||||
void (*mCopy)(int8_t *dst, const int *const *src, unsigned bytesPerSample,
|
||||
unsigned nSamples, unsigned nChannels);
|
||||
|
||||
// handle to underlying libFLAC parser
|
||||
FLAC__StreamDecoder *mDecoder;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.google.vr:sdk-audio:1.60.1'
|
||||
compile 'com.google.vr:sdk-audio:1.80.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -138,6 +138,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||
return C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputSampleRateHz() {
|
||||
return sampleRateHz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer input) {
|
||||
int position = input.position();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# ExoPlayer IMA extension #
|
||||
|
||||
The IMA extension is a [MediaSource][] implementation wrapping the
|
||||
The IMA extension is an [AdsLoader][] implementation wrapping the
|
||||
[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads
|
||||
alongside content.
|
||||
|
||||
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
|
||||
[MediaSource]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/MediaSource.html
|
||||
[AdsLoader]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
|||
## Using the extension ##
|
||||
|
||||
To play ads alongside a single-window content `MediaSource`, prepare the player
|
||||
with an `ImaAdsMediaSource` constructed using an `ImaAdsLoader`, the content
|
||||
with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content
|
||||
`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag
|
||||
URI from your ad campaign when creating the `ImaAdsLoader`. The IMA
|
||||
documentation includes some [sample ad tags][] for testing.
|
||||
|
|
@ -38,7 +38,7 @@ background, and are recreated when the player returns to the foreground. When
|
|||
playing ads it is necessary to persist ad playback state while in the background
|
||||
by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of
|
||||
the same content/ads by passing it in when constructing the new
|
||||
`ImaAdsMediaSource`. It is also important to persist the player position when
|
||||
`AdsMediaSource`. It is also important to persist the player position when
|
||||
entering the background by storing the value of `player.getContentPosition()`.
|
||||
On returning to the foreground, seek to that position before preparing the new
|
||||
player instance. Finally, it is important to call `ImaAdsLoader.release()` when
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
@ -28,14 +28,15 @@ android {
|
|||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
// This dependency is necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via:
|
||||
// com.google.android.gms:play-services-ads:11.0.2
|
||||
// |-- com.google.android.gms:play-services-ads-lite:[11.0.2] -> 11.0.2
|
||||
// |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2
|
||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
||||
// is included via:
|
||||
// com.google.android.gms:play-services-ads:11.4.2
|
||||
// |-- com.google.android.gms:play-services-ads-lite:11.4.2
|
||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
compile 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
|
||||
compile 'com.google.android.gms:play-services-ads:11.0.2'
|
||||
compile 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
||||
androidTestCompile project(modulePrefix + 'library')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.ima;
|
|||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
|
|
@ -29,7 +30,6 @@ import com.google.ads.interactivemedia.v3.api.AdEvent;
|
|||
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
|
||||
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
|
||||
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsLoader;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsManager;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
|
||||
|
|
@ -44,13 +44,14 @@ import com.google.android.exoplayer2.C;
|
|||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -58,39 +59,8 @@ import java.util.Map;
|
|||
/**
|
||||
* Loads ads using the IMA SDK. All methods are called on the main thread.
|
||||
*/
|
||||
public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
||||
ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
|
||||
|
||||
/**
|
||||
* Listener for ad loader events. All methods are called on the main thread.
|
||||
*/
|
||||
/* package */ interface EventListener {
|
||||
|
||||
/**
|
||||
* Called when the ad playback state has been updated.
|
||||
*
|
||||
* @param adPlaybackState The new ad playback state.
|
||||
*/
|
||||
void onAdPlaybackState(AdPlaybackState adPlaybackState);
|
||||
|
||||
/**
|
||||
* Called when there was an error loading ads.
|
||||
*
|
||||
* @param error The error.
|
||||
*/
|
||||
void onLoadError(IOException error);
|
||||
|
||||
/**
|
||||
* Called when the user clicks through an ad (for example, following a 'learn more' link).
|
||||
*/
|
||||
void onAdClicked();
|
||||
|
||||
/**
|
||||
* Called when the user taps a non-clickthrough part of an ad.
|
||||
*/
|
||||
void onAdTapped();
|
||||
|
||||
}
|
||||
public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader,
|
||||
VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
|
||||
|
|
@ -121,12 +91,31 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
|
||||
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
|
||||
|
||||
/**
|
||||
* The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
|
||||
private @interface ImaAdState {}
|
||||
/**
|
||||
* The ad playback state when IMA is not playing an ad.
|
||||
*/
|
||||
private static final int IMA_AD_STATE_NONE = 0;
|
||||
/**
|
||||
* The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}.
|
||||
*/
|
||||
private static final int IMA_AD_STATE_PLAYING = 1;
|
||||
/**
|
||||
* The ad playback state when IMA has called {@link #pauseAd()} while playing an ad.
|
||||
*/
|
||||
private static final int IMA_AD_STATE_PAUSED = 2;
|
||||
|
||||
private final Uri adTagUri;
|
||||
private final Timeline.Period period;
|
||||
private final List<VideoAdPlayerCallback> adCallbacks;
|
||||
private final ImaSdkFactory imaSdkFactory;
|
||||
private final AdDisplayContainer adDisplayContainer;
|
||||
private final AdsLoader adsLoader;
|
||||
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
||||
|
||||
private EventListener eventListener;
|
||||
private Player player;
|
||||
|
|
@ -150,26 +139,17 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
*/
|
||||
private boolean imaPausedContent;
|
||||
/**
|
||||
* If {@link #playingAd} is set, stores whether IMA has called {@link #playAd()} and not
|
||||
* {@link #stopAd()}.
|
||||
* The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}.
|
||||
*/
|
||||
private boolean imaPlayingAd;
|
||||
private @ImaAdState int imaAdState;
|
||||
/**
|
||||
* If {@link #playingAd} is set, stores whether IMA has called {@link #pauseAd()} since a
|
||||
* preceding call to {@link #playAd()} for the current ad.
|
||||
*/
|
||||
private boolean imaPausedInAd;
|
||||
/**
|
||||
* Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
|
||||
* Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been
|
||||
* called since starting ad playback.
|
||||
*/
|
||||
private boolean sentContentComplete;
|
||||
|
||||
// Fields tracking the player/loader state.
|
||||
|
||||
/**
|
||||
* Whether the player's play when ready flag has temporarily been set to true for playing ads.
|
||||
*/
|
||||
private boolean playWhenReadyOverriddenForAds;
|
||||
/**
|
||||
* Whether the player is playing an ad.
|
||||
*/
|
||||
|
|
@ -249,14 +229,17 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
|
||||
/**
|
||||
* Attaches a player that will play ads loaded using this instance.
|
||||
*
|
||||
* @param player The player instance that will play the loaded ads.
|
||||
* @param eventListener Listener for ads loader events.
|
||||
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
|
||||
* Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by
|
||||
* this instance.
|
||||
*/
|
||||
/* package */ void attachPlayer(ExoPlayer player, EventListener eventListener,
|
||||
ViewGroup adUiViewGroup) {
|
||||
public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() {
|
||||
return adsLoader;
|
||||
}
|
||||
|
||||
// AdsLoader implementation.
|
||||
|
||||
@Override
|
||||
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
|
||||
this.player = player;
|
||||
this.eventListener = eventListener;
|
||||
this.adUiViewGroup = adUiViewGroup;
|
||||
|
|
@ -265,8 +248,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||
player.addListener(this);
|
||||
if (adPlaybackState != null) {
|
||||
eventListener.onAdPlaybackState(adPlaybackState);
|
||||
if (imaPausedContent) {
|
||||
eventListener.onAdPlaybackState(adPlaybackState.copy());
|
||||
if (imaPausedContent && player.getPlayWhenReady()) {
|
||||
adsManager.resume();
|
||||
}
|
||||
} else {
|
||||
|
|
@ -274,14 +257,10 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the attached player and event listener. To attach a new player, call
|
||||
* {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Call {@link #release()} to release
|
||||
* all resources associated with this instance.
|
||||
*/
|
||||
/* package */ void detachPlayer() {
|
||||
@Override
|
||||
public void detachPlayer() {
|
||||
if (adsManager != null && imaPausedContent) {
|
||||
adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition()));
|
||||
adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
|
||||
adsManager.pause();
|
||||
}
|
||||
lastAdProgress = getAdProgress();
|
||||
|
|
@ -292,9 +271,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
adUiViewGroup = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the loader. Must be called when the instance is no longer needed.
|
||||
*/
|
||||
@Override
|
||||
public void release() {
|
||||
released = true;
|
||||
if (adsManager != null) {
|
||||
|
|
@ -303,7 +280,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
}
|
||||
|
||||
// AdsLoader.AdsLoadedListener implementation.
|
||||
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
|
||||
|
||||
@Override
|
||||
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
|
||||
|
|
@ -477,28 +454,32 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "playAd");
|
||||
}
|
||||
switch (imaAdState) {
|
||||
case IMA_AD_STATE_PLAYING:
|
||||
// IMA does not always call stopAd before resuming content.
|
||||
// See [Internal: b/38354028, b/63320878].
|
||||
Log.w(TAG, "Unexpected playAd without stopAd");
|
||||
break;
|
||||
case IMA_AD_STATE_NONE:
|
||||
imaAdState = IMA_AD_STATE_PLAYING;
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPlay();
|
||||
}
|
||||
break;
|
||||
case IMA_AD_STATE_PAUSED:
|
||||
imaAdState = IMA_AD_STATE_PLAYING;
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onResume();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (player == null) {
|
||||
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
|
||||
Log.w(TAG, "Unexpected playAd while detached");
|
||||
} else if (!player.getPlayWhenReady()) {
|
||||
playWhenReadyOverriddenForAds = true;
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
if (imaPlayingAd && !imaPausedInAd) {
|
||||
// Work around an issue where IMA does not always call stopAd before resuming content.
|
||||
// See [Internal: b/38354028, b/63320878].
|
||||
Log.w(TAG, "Unexpected playAd without stopAd");
|
||||
}
|
||||
if (!imaPlayingAd) {
|
||||
imaPlayingAd = true;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onPlay();
|
||||
}
|
||||
} else if (imaPausedInAd) {
|
||||
imaPausedInAd = false;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onResume();
|
||||
}
|
||||
adsManager.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -511,7 +492,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
|
||||
Log.w(TAG, "Unexpected stopAd while detached");
|
||||
}
|
||||
if (!imaPlayingAd) {
|
||||
if (imaAdState == IMA_AD_STATE_NONE) {
|
||||
Log.w(TAG, "Unexpected stopAd");
|
||||
return;
|
||||
}
|
||||
|
|
@ -523,13 +504,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "pauseAd");
|
||||
}
|
||||
if (!imaPlayingAd) {
|
||||
if (imaAdState == IMA_AD_STATE_NONE) {
|
||||
// This method is called after content is resumed.
|
||||
return;
|
||||
}
|
||||
imaPausedInAd = true;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onPause();
|
||||
imaAdState = IMA_AD_STATE_PAUSED;
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPause();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -549,53 +530,53 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
this.timeline = timeline;
|
||||
contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs);
|
||||
long contentDurationUs = timeline.getPeriod(0, period).durationUs;
|
||||
contentDurationMs = C.usToMs(contentDurationUs);
|
||||
if (contentDurationUs != C.TIME_UNSET) {
|
||||
adPlaybackState.contentDurationUs = contentDurationUs;
|
||||
}
|
||||
updateImaStateForPlayerState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (adsManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imaPlayingAd && playbackState == Player.STATE_BUFFERING && playWhenReady) {
|
||||
if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {
|
||||
adsManager.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) {
|
||||
adsManager.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING
|
||||
&& playWhenReady) {
|
||||
checkForContentComplete();
|
||||
} else if (imaPlayingAd && playbackState == Player.STATE_ENDED) {
|
||||
} else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) {
|
||||
// IMA is waiting for the ad playback to finish so invoke the callback now.
|
||||
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onEnded();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
if (playingAd) {
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onError();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (adsManager == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -621,11 +602,6 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void requestAds() {
|
||||
|
|
@ -639,24 +615,19 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
private void updateImaStateForPlayerState() {
|
||||
boolean wasPlayingAd = playingAd;
|
||||
playingAd = player.isPlayingAd();
|
||||
if (!playingAd && playWhenReadyOverriddenForAds) {
|
||||
playWhenReadyOverriddenForAds = false;
|
||||
player.setPlayWhenReady(false);
|
||||
}
|
||||
if (!sentContentComplete) {
|
||||
boolean adFinished = (wasPlayingAd && !playingAd)
|
||||
|| playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup();
|
||||
if (adFinished) {
|
||||
// IMA is waiting for the ad playback to finish so invoke the callback now.
|
||||
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onEnded();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
}
|
||||
if (!wasPlayingAd && playingAd) {
|
||||
int adGroupIndex = player.getCurrentAdGroupIndex();
|
||||
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
|
||||
Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
|
||||
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
|
||||
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
|
||||
|
|
@ -668,7 +639,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
|
||||
private void resumeContentInternal() {
|
||||
if (imaPlayingAd) {
|
||||
if (imaAdState != IMA_AD_STATE_NONE) {
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
|
||||
}
|
||||
|
|
@ -678,10 +650,10 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
adGroupIndex = C.INDEX_UNSET;
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
clearFlags();
|
||||
}
|
||||
|
||||
private void pauseContentInternal() {
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
if (sentPendingContentPositionMs) {
|
||||
pendingContentPositionMs = C.TIME_UNSET;
|
||||
sentPendingContentPositionMs = false;
|
||||
|
|
@ -689,24 +661,16 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
// IMA is requesting to pause content, so stop faking the content position.
|
||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
||||
clearFlags();
|
||||
}
|
||||
|
||||
private void stopAdInternal() {
|
||||
Assertions.checkState(imaPlayingAd);
|
||||
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
adPlaybackState.playedAd(adGroupIndex);
|
||||
updateAdPlaybackState();
|
||||
if (!playingAd) {
|
||||
adGroupIndex = C.INDEX_UNSET;
|
||||
}
|
||||
clearFlags();
|
||||
}
|
||||
|
||||
private void clearFlags() {
|
||||
// If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until
|
||||
// the content is resumed.
|
||||
imaPlayingAd = false;
|
||||
imaPausedInAd = false;
|
||||
}
|
||||
|
||||
private void checkForContentComplete() {
|
||||
|
|
@ -728,6 +692,15 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
}
|
||||
|
||||
private void focusSkipButton() {
|
||||
if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0
|
||||
&& adUiViewGroup.getChildAt(0) instanceof WebView) {
|
||||
WebView webView = (WebView) (adUiViewGroup.getChildAt(0));
|
||||
webView.requestFocus();
|
||||
webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS);
|
||||
}
|
||||
}
|
||||
|
||||
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
|
||||
if (cuePoints.isEmpty()) {
|
||||
// If no cue points are specified, there is a preroll ad.
|
||||
|
|
@ -744,13 +717,4 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
return adGroupTimesUs;
|
||||
}
|
||||
|
||||
private void focusSkipButton() {
|
||||
if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0
|
||||
&& adUiViewGroup.getChildAt(0) instanceof WebView) {
|
||||
WebView webView = (WebView) (adUiViewGroup.getChildAt(0));
|
||||
webView.requestFocus();
|
||||
webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,83 +16,26 @@
|
|||
package com.google.android.exoplayer2.ext.ima;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link MediaSource} that inserts ads linearly with a provided content media source using the
|
||||
* Interactive Media Ads SDK for ad loading and tracking.
|
||||
* A {@link MediaSource} that inserts ads linearly with a provided content media source.
|
||||
*
|
||||
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class ImaAdsMediaSource implements MediaSource {
|
||||
|
||||
/**
|
||||
* Listener for events relating to ad loading.
|
||||
*/
|
||||
public interface AdsListener {
|
||||
|
||||
/**
|
||||
* Called if there was an error loading ads. The media source will load the content without ads
|
||||
* if ads can't be loaded, so listen for this event if you need to implement additional handling
|
||||
* (for example, stopping the player).
|
||||
*
|
||||
* @param error The error.
|
||||
*/
|
||||
void onAdLoadError(IOException error);
|
||||
|
||||
/**
|
||||
* Called when the user clicks through an ad (for example, following a 'learn more' link).
|
||||
*/
|
||||
void onAdClicked();
|
||||
|
||||
/**
|
||||
* Called when the user taps a non-clickthrough part of an ad.
|
||||
*/
|
||||
void onAdTapped();
|
||||
|
||||
}
|
||||
|
||||
private static final String TAG = "ImaAdsMediaSource";
|
||||
|
||||
private final MediaSource contentMediaSource;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final ImaAdsLoader imaAdsLoader;
|
||||
private final ViewGroup adUiViewGroup;
|
||||
private final Handler mainHandler;
|
||||
private final AdsLoaderListener adsLoaderListener;
|
||||
private final Map<MediaPeriod, MediaSource> adMediaSourceByMediaPeriod;
|
||||
private final Timeline.Period period;
|
||||
@Nullable
|
||||
private final Handler eventHandler;
|
||||
@Nullable
|
||||
private final AdsListener eventListener;
|
||||
|
||||
private Handler playerHandler;
|
||||
private ExoPlayer player;
|
||||
private volatile boolean released;
|
||||
|
||||
// Accessed on the player thread.
|
||||
private Timeline contentTimeline;
|
||||
private Object contentManifest;
|
||||
private AdPlaybackState adPlaybackState;
|
||||
private MediaSource[][] adGroupMediaSources;
|
||||
private long[][] adDurationsUs;
|
||||
private MediaSource.Listener listener;
|
||||
private final AdsMediaSource adsMediaSource;
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
|
|
@ -121,230 +64,41 @@ public final class ImaAdsMediaSource implements MediaSource {
|
|||
*/
|
||||
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
|
||||
ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
|
||||
@Nullable AdsListener eventListener) {
|
||||
this.contentMediaSource = contentMediaSource;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.imaAdsLoader = imaAdsLoader;
|
||||
this.adUiViewGroup = adUiViewGroup;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
mainHandler = new Handler(Looper.getMainLooper());
|
||||
adsLoaderListener = new AdsLoaderListener();
|
||||
adMediaSourceByMediaPeriod = new HashMap<>();
|
||||
period = new Timeline.Period();
|
||||
adGroupMediaSources = new MediaSource[0][];
|
||||
adDurationsUs = new long[0][];
|
||||
@Nullable AdsMediaSource.AdsListener eventListener) {
|
||||
adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
|
||||
adUiViewGroup, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
Assertions.checkArgument(isTopLevelSource);
|
||||
this.listener = listener;
|
||||
this.player = player;
|
||||
playerHandler = new Handler();
|
||||
contentMediaSource.prepareSource(player, false, new Listener() {
|
||||
public void prepareSource(final ExoPlayer player, boolean isTopLevelSource,
|
||||
final Listener listener) {
|
||||
adsMediaSource.prepareSource(player, false, new Listener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
|
||||
ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest);
|
||||
}
|
||||
});
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
imaAdsLoader.attachPlayer(player, adsLoaderListener, adUiViewGroup);
|
||||
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
|
||||
@Nullable Object manifest) {
|
||||
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
contentMediaSource.maybeThrowSourceInfoRefreshError();
|
||||
for (MediaSource[] mediaSources : adGroupMediaSources) {
|
||||
for (MediaSource mediaSource : mediaSources) {
|
||||
if (mediaSource != null) {
|
||||
mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
}
|
||||
}
|
||||
adsMediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
|
||||
final int adGroupIndex = id.adGroupIndex;
|
||||
final int adIndexInAdGroup = id.adIndexInAdGroup;
|
||||
if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
|
||||
MediaSource adMediaSource = new ExtractorMediaSource(
|
||||
adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory,
|
||||
new DefaultExtractorsFactory(), mainHandler, adsLoaderListener);
|
||||
int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
|
||||
if (adIndexInAdGroup >= oldAdCount) {
|
||||
int adCount = adIndexInAdGroup + 1;
|
||||
adGroupMediaSources[adGroupIndex] =
|
||||
Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
|
||||
adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
|
||||
Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
|
||||
}
|
||||
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
|
||||
adMediaSource.prepareSource(player, false, new Listener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
|
||||
onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
|
||||
}
|
||||
});
|
||||
}
|
||||
MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
|
||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
|
||||
adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
|
||||
return mediaPeriod;
|
||||
} else {
|
||||
return contentMediaSource.createPeriod(id, allocator);
|
||||
}
|
||||
return adsMediaSource.createPeriod(id, allocator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
|
||||
adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
|
||||
} else {
|
||||
contentMediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
adsMediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
released = true;
|
||||
contentMediaSource.releaseSource();
|
||||
for (MediaSource[] mediaSources : adGroupMediaSources) {
|
||||
for (MediaSource mediaSource : mediaSources) {
|
||||
if (mediaSource != null) {
|
||||
mediaSource.releaseSource();
|
||||
}
|
||||
}
|
||||
}
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
imaAdsLoader.detachPlayer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void onAdPlaybackState(AdPlaybackState adPlaybackState) {
|
||||
if (this.adPlaybackState == null) {
|
||||
adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][];
|
||||
Arrays.fill(adGroupMediaSources, new MediaSource[0]);
|
||||
adDurationsUs = new long[adPlaybackState.adGroupCount][];
|
||||
Arrays.fill(adDurationsUs, new long[0]);
|
||||
}
|
||||
this.adPlaybackState = adPlaybackState;
|
||||
maybeUpdateSourceInfo();
|
||||
}
|
||||
|
||||
private void onLoadError(final IOException error) {
|
||||
Log.w(TAG, "Ad load error", error);
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!released) {
|
||||
eventListener.onAdLoadError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) {
|
||||
contentTimeline = timeline;
|
||||
contentManifest = manifest;
|
||||
maybeUpdateSourceInfo();
|
||||
}
|
||||
|
||||
private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
|
||||
maybeUpdateSourceInfo();
|
||||
}
|
||||
|
||||
private void maybeUpdateSourceInfo() {
|
||||
if (adPlaybackState != null && contentTimeline != null) {
|
||||
Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline
|
||||
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs,
|
||||
adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts,
|
||||
adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs);
|
||||
listener.onSourceInfoRefreshed(timeline, contentManifest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for ad loading events. All methods are called on the main thread.
|
||||
*/
|
||||
private final class AdsLoaderListener implements ImaAdsLoader.EventListener,
|
||||
ExtractorMediaSource.EventListener {
|
||||
|
||||
@Override
|
||||
public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
playerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
ImaAdsMediaSource.this.onAdPlaybackState(adPlaybackState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(final IOException error) {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
playerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
ImaAdsMediaSource.this.onLoadError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdClicked() {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!released) {
|
||||
eventListener.onAdClicked();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdTapped() {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!released) {
|
||||
eventListener.onAdTapped();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
adsMediaSource.releaseSource();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
31
extensions/leanback/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# ExoPlayer Leanback extension #
|
||||
|
||||
This [Leanback][] Extension provides a [PlayerAdapter][] implementation for
|
||||
ExoPlayer.
|
||||
|
||||
[PlayerAdapter]: https://developer.android.com/reference/android/support/v17/leanback/media/PlayerAdapter.html
|
||||
[Leanback]: https://developer.android.com/reference/android/support/v17/leanback/package-summary.html
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-leanback:rX.X.X'
|
||||
```
|
||||
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
41
extensions/leanback/build.gradle
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile('com.android.support:leanback-v17:' + supportLibraryVersion)
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Leanback extension for Exoplayer library'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-leanback'
|
||||
releaseDescription = 'Leanback extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
||||
17
extensions/leanback/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.leanback"/>
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.leanback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v17.leanback.R;
|
||||
import android.support.v17.leanback.media.PlaybackGlueHost;
|
||||
import android.support.v17.leanback.media.PlayerAdapter;
|
||||
import android.support.v17.leanback.media.SurfaceHolderGlueHost;
|
||||
import android.util.Pair;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
|
||||
/**
|
||||
* Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}.
|
||||
*/
|
||||
public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final SimpleExoPlayer player;
|
||||
private final Handler handler;
|
||||
private final ComponentListener componentListener;
|
||||
private final Runnable updateProgressRunnable;
|
||||
|
||||
private ControlDispatcher controlDispatcher;
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||
private boolean hasSurface;
|
||||
private boolean lastNotifiedPreparedState;
|
||||
|
||||
/**
|
||||
* Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the
|
||||
* {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when
|
||||
* it's no longer required.
|
||||
*
|
||||
* @param context The current context (activity).
|
||||
* @param player Instance of your exoplayer that needs to be configured.
|
||||
* @param updatePeriodMs The delay between player control updates, in milliseconds.
|
||||
*/
|
||||
public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) {
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
handler = new Handler();
|
||||
componentListener = new ComponentListener();
|
||||
controlDispatcher = new DefaultControlDispatcher();
|
||||
updateProgressRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Callback callback = getCallback();
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
||||
handler.postDelayed(this, updatePeriodMs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ControlDispatcher}.
|
||||
*
|
||||
* @param controlDispatcher The {@link ControlDispatcher}, or null to use
|
||||
* {@link DefaultControlDispatcher}.
|
||||
*/
|
||||
public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {
|
||||
this.controlDispatcher = controlDispatcher == null ? new DefaultControlDispatcher()
|
||||
: controlDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional {@link ErrorMessageProvider}.
|
||||
*
|
||||
* @param errorMessageProvider The {@link ErrorMessageProvider}.
|
||||
*/
|
||||
public void setErrorMessageProvider(
|
||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
this.errorMessageProvider = errorMessageProvider;
|
||||
}
|
||||
|
||||
// PlayerAdapter implementation.
|
||||
|
||||
@Override
|
||||
public void onAttachedToHost(PlaybackGlueHost host) {
|
||||
if (host instanceof SurfaceHolderGlueHost) {
|
||||
surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);
|
||||
}
|
||||
notifyStateChanged();
|
||||
player.addListener(componentListener);
|
||||
player.addVideoListener(componentListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromHost() {
|
||||
player.removeListener(componentListener);
|
||||
player.removeVideoListener(componentListener);
|
||||
if (surfaceHolderGlueHost != null) {
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
||||
surfaceHolderGlueHost = null;
|
||||
}
|
||||
hasSurface = false;
|
||||
Callback callback = getCallback();
|
||||
callback.onBufferingStateChanged(this, false);
|
||||
callback.onPlayStateChanged(this);
|
||||
maybeNotifyPreparedStateChanged(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProgressUpdatingEnabled(boolean enabled) {
|
||||
handler.removeCallbacks(updateProgressRunnable);
|
||||
if (enabled) {
|
||||
handler.post(updateProgressRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
int playbackState = player.getPlaybackState();
|
||||
return playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||
&& player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
long durationMs = player.getDuration();
|
||||
return durationMs == C.TIME_UNSET ? -1 : durationMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||
}
|
||||
if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) {
|
||||
getCallback().onPlayStateChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
if (controlDispatcher.dispatchSetPlayWhenReady(player, false)) {
|
||||
getCallback().onPlayStateChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(long positionMs) {
|
||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), positionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPosition() {
|
||||
return player.getBufferedPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPrepared() {
|
||||
return player.getPlaybackState() != Player.STATE_IDLE
|
||||
&& (surfaceHolderGlueHost == null || hasSurface);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
/* package */ void setVideoSurface(Surface surface) {
|
||||
hasSurface = surface != null;
|
||||
player.setVideoSurface(surface);
|
||||
maybeNotifyPreparedStateChanged(getCallback());
|
||||
}
|
||||
|
||||
/* package */ void notifyStateChanged() {
|
||||
int playbackState = player.getPlaybackState();
|
||||
Callback callback = getCallback();
|
||||
maybeNotifyPreparedStateChanged(callback);
|
||||
callback.onPlayStateChanged(this);
|
||||
callback.onBufferingStateChanged(this, playbackState == Player.STATE_BUFFERING);
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
callback.onPlayCompleted(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyPreparedStateChanged(Callback callback) {
|
||||
boolean isPrepared = isPrepared();
|
||||
if (lastNotifiedPreparedState != isPrepared) {
|
||||
lastNotifiedPreparedState = isPrepared;
|
||||
callback.onPreparedStateChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
private final class ComponentListener extends Player.DefaultEventListener implements
|
||||
SimpleExoPlayer.VideoListener, SurfaceHolder.Callback {
|
||||
|
||||
// SurfaceHolder.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder surfaceHolder) {
|
||||
setVideoSurface(surfaceHolder.getSurface());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
|
||||
setVideoSurface(null);
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
notifyStateChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException exception) {
|
||||
Callback callback = getCallback();
|
||||
if (errorMessageProvider != null) {
|
||||
Pair<Integer, String> errorMessage = errorMessageProvider.getErrorMessage(exception);
|
||||
callback.onError(LeanbackPlayerAdapter.this, errorMessage.first, errorMessage.second);
|
||||
} else {
|
||||
callback.onError(LeanbackPlayerAdapter.this, exception.type, context.getString(
|
||||
R.string.lb_media_player_error, exception.type, exception.rendererIndex));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
Callback callback = getCallback();
|
||||
callback.onDurationChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
Callback callback = getCallback();
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
||||
}
|
||||
|
||||
// SimpleExoplayerView.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,7 +27,6 @@ android {
|
|||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-media-compat:' + supportLibraryVersion
|
||||
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.mediasession;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link MediaSessionConnector.PlaybackController}.
|
||||
|
|
@ -38,33 +40,37 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
|
|||
|
||||
private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||
| PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
|
||||
| PlaybackStateCompat.ACTION_STOP;
|
||||
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
|
||||
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
|
||||
|
||||
protected final long rewindIncrementMs;
|
||||
protected final long fastForwardIncrementMs;
|
||||
protected final int repeatToggleModes;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* <p>
|
||||
* Equivalent to {@code DefaultPlaybackController(
|
||||
* DefaultPlaybackController.DEFAULT_REWIND_MS,
|
||||
* DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}.
|
||||
* Equivalent to {@code DefaultPlaybackController(DefaultPlaybackController.DEFAULT_REWIND_MS,
|
||||
* DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS,
|
||||
* MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
|
||||
*/
|
||||
public DefaultPlaybackController() {
|
||||
this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS);
|
||||
this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS,
|
||||
MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given fast forward and rewind increments.
|
||||
*
|
||||
* @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
|
||||
* @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
|
||||
* cause the rewind action to be disabled.
|
||||
* @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative
|
||||
* value will cause the fast forward action to be removed.
|
||||
* @param repeatToggleModes The available repeatToggleModes.
|
||||
*/
|
||||
public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) {
|
||||
public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs,
|
||||
@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
|
||||
this.rewindIncrementMs = rewindIncrementMs;
|
||||
this.fastForwardIncrementMs = fastForwardIncrementMs;
|
||||
this.repeatToggleModes = repeatToggleModes;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -125,4 +131,44 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
|
|||
player.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffleMode(Player player, int shuffleMode) {
|
||||
player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
|| shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRepeatMode(Player player, int repeatMode) {
|
||||
int selectedExoPlayerRepeatMode = player.getRepeatMode();
|
||||
switch (repeatMode) {
|
||||
case PlaybackStateCompat.REPEAT_MODE_ALL:
|
||||
case PlaybackStateCompat.REPEAT_MODE_GROUP:
|
||||
if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) != 0) {
|
||||
selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ALL;
|
||||
}
|
||||
break;
|
||||
case PlaybackStateCompat.REPEAT_MODE_ONE:
|
||||
if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE) != 0) {
|
||||
selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ONE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
selectedExoPlayerRepeatMode = Player.REPEAT_MODE_OFF;
|
||||
break;
|
||||
}
|
||||
player.setRepeatMode(selectedExoPlayerRepeatMode);
|
||||
}
|
||||
|
||||
// CommandReceiver implementation.
|
||||
|
||||
@Override
|
||||
public String[] getCommands() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
|||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -72,16 +72,37 @@ public final class MediaSessionConnector {
|
|||
ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
|
||||
}
|
||||
|
||||
/**
|
||||
* The default repeat toggle modes which is the bitmask of
|
||||
* {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and
|
||||
* {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
|
||||
*/
|
||||
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
|
||||
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
|
||||
public static final String EXTRAS_PITCH = "EXO_PITCH";
|
||||
private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
|
||||
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
|
||||
private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
|
||||
| MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
|
||||
|
||||
/**
|
||||
* Receiver of media commands sent by a media controller.
|
||||
*/
|
||||
public interface CommandReceiver {
|
||||
/**
|
||||
* Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
|
||||
*/
|
||||
String[] getCommands();
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
|
||||
*/
|
||||
void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to which playback preparation actions are delegated.
|
||||
*/
|
||||
public interface PlaybackPreparer {
|
||||
public interface PlaybackPreparer extends CommandReceiver {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_PREPARE
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
|
||||
|
|
@ -120,28 +141,27 @@ public final class MediaSessionConnector {
|
|||
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
|
||||
*/
|
||||
void onPrepareFromUri(Uri uri, Bundle extras);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
|
||||
*/
|
||||
void onCommand(String command, Bundle extras, ResultReceiver cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to which playback actions are delegated.
|
||||
*/
|
||||
public interface PlaybackController {
|
||||
public interface PlaybackController extends CommandReceiver {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
|
||||
| PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
|
||||
| PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
|
||||
| PlaybackStateCompat.ACTION_STOP;
|
||||
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
||||
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
|
||||
|
||||
/**
|
||||
* Returns the actions which are supported by the controller. The supported actions must be a
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE},
|
||||
* {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE},
|
||||
* {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD},
|
||||
* {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}.
|
||||
* {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP},
|
||||
* {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and
|
||||
* {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
|
||||
*
|
||||
* @param player The player.
|
||||
* @return The bitmask of the supported media actions.
|
||||
|
|
@ -171,24 +191,30 @@ public final class MediaSessionConnector {
|
|||
* See {@link MediaSessionCompat.Callback#onStop()}.
|
||||
*/
|
||||
void onStop(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}.
|
||||
*/
|
||||
void onSetShuffleMode(Player player, int shuffleMode);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}.
|
||||
*/
|
||||
void onSetRepeatMode(Player player, int repeatMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles queue navigation actions, and updates the media session queue by calling
|
||||
* {@code MediaSessionCompat.setQueue()}.
|
||||
*/
|
||||
public interface QueueNavigator {
|
||||
public interface QueueNavigator extends CommandReceiver {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED;
|
||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||
|
||||
/**
|
||||
* Returns the actions which are supported by the navigator. The supported actions must be a
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM},
|
||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT},
|
||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS},
|
||||
* {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}.
|
||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
|
||||
*
|
||||
* @param player The {@link Player}.
|
||||
* @return The bitmask of the supported media actions.
|
||||
|
|
@ -230,16 +256,12 @@ public final class MediaSessionConnector {
|
|||
* See {@link MediaSessionCompat.Callback#onSkipToNext()}.
|
||||
*/
|
||||
void onSkipToNext(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}.
|
||||
*/
|
||||
void onSetShuffleModeEnabled(Player player, boolean enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles media session queue edits.
|
||||
*/
|
||||
public interface QueueEditor {
|
||||
public interface QueueEditor extends CommandReceiver {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
|
||||
|
||||
|
|
@ -297,17 +319,6 @@ public final class MediaSessionConnector {
|
|||
PlaybackStateCompat.CustomAction getCustomAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an exception into an error code and a user readable error message.
|
||||
*/
|
||||
public interface ErrorMessageProvider {
|
||||
/**
|
||||
* Returns a pair consisting of an error code and a user readable error message for a given
|
||||
* exception.
|
||||
*/
|
||||
Pair<Integer, String> getErrorMessage(ExoPlaybackException playbackException);
|
||||
}
|
||||
|
||||
/**
|
||||
* The wrapped {@link MediaSessionCompat}.
|
||||
*/
|
||||
|
|
@ -319,11 +330,12 @@ public final class MediaSessionConnector {
|
|||
private final ExoPlayerEventListener exoPlayerEventListener;
|
||||
private final MediaSessionCallback mediaSessionCallback;
|
||||
private final PlaybackController playbackController;
|
||||
private final Map<String, CommandReceiver> commandMap;
|
||||
|
||||
private Player player;
|
||||
private CustomActionProvider[] customActionProviders;
|
||||
private Map<String, CustomActionProvider> customActionMap;
|
||||
private ErrorMessageProvider errorMessageProvider;
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private PlaybackPreparer playbackPreparer;
|
||||
private QueueNavigator queueNavigator;
|
||||
private QueueEditor queueEditor;
|
||||
|
|
@ -338,7 +350,7 @@ public final class MediaSessionConnector {
|
|||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||
*/
|
||||
public MediaSessionConnector(MediaSessionCompat mediaSession) {
|
||||
this(mediaSession, new DefaultPlaybackController());
|
||||
this(mediaSession, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -360,7 +372,8 @@ public final class MediaSessionConnector {
|
|||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
||||
*
|
||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||
* @param playbackController A {@link PlaybackController} for handling playback actions.
|
||||
* @param playbackController A {@link PlaybackController} for handling playback actions, or
|
||||
* {@code null} if the connector should handle playback actions directly.
|
||||
* @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
|
||||
* {@code false}, you need to maintain the metadata of the media session yourself (provide at
|
||||
* least the duration to allow clients to show a progress bar).
|
||||
|
|
@ -368,7 +381,8 @@ public final class MediaSessionConnector {
|
|||
public MediaSessionConnector(MediaSessionCompat mediaSession,
|
||||
PlaybackController playbackController, boolean doMaintainMetadata) {
|
||||
this.mediaSession = mediaSession;
|
||||
this.playbackController = playbackController;
|
||||
this.playbackController = playbackController != null ? playbackController
|
||||
: new DefaultPlaybackController();
|
||||
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
|
||||
: Looper.getMainLooper());
|
||||
this.doMaintainMetadata = doMaintainMetadata;
|
||||
|
|
@ -377,6 +391,8 @@ public final class MediaSessionConnector {
|
|||
mediaSessionCallback = new MediaSessionCallback();
|
||||
exoPlayerEventListener = new ExoPlayerEventListener();
|
||||
customActionMap = Collections.emptyMap();
|
||||
commandMap = new HashMap<>();
|
||||
registerCommandReceiver(playbackController);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -396,8 +412,12 @@ public final class MediaSessionConnector {
|
|||
this.player.removeListener(exoPlayerEventListener);
|
||||
mediaSession.setCallback(null);
|
||||
}
|
||||
this.playbackPreparer = playbackPreparer;
|
||||
unregisterCommandReceiver(this.playbackPreparer);
|
||||
|
||||
this.player = player;
|
||||
this.playbackPreparer = playbackPreparer;
|
||||
registerCommandReceiver(playbackPreparer);
|
||||
|
||||
this.customActionProviders = (player != null && customActionProviders != null)
|
||||
? customActionProviders : new CustomActionProvider[0];
|
||||
if (player != null) {
|
||||
|
|
@ -413,19 +433,21 @@ public final class MediaSessionConnector {
|
|||
*
|
||||
* @param errorMessageProvider The error message provider.
|
||||
*/
|
||||
public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) {
|
||||
public void setErrorMessageProvider(
|
||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
this.errorMessageProvider = errorMessageProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT},
|
||||
* {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and
|
||||
* {@code ACTION_SET_SHUFFLE_MODE_ENABLED}.
|
||||
* {@code ACTION_SKIP_TO_PREVIOUS} and {@code ACTION_SKIP_TO_QUEUE_ITEM}.
|
||||
*
|
||||
* @param queueNavigator The queue navigator.
|
||||
*/
|
||||
public void setQueueNavigator(QueueNavigator queueNavigator) {
|
||||
unregisterCommandReceiver(this.queueNavigator);
|
||||
this.queueNavigator = queueNavigator;
|
||||
registerCommandReceiver(queueNavigator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -434,11 +456,29 @@ public final class MediaSessionConnector {
|
|||
* @param queueEditor The queue editor.
|
||||
*/
|
||||
public void setQueueEditor(QueueEditor queueEditor) {
|
||||
unregisterCommandReceiver(this.queueEditor);
|
||||
this.queueEditor = queueEditor;
|
||||
registerCommandReceiver(queueEditor);
|
||||
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
|
||||
: EDITOR_MEDIA_SESSION_FLAGS);
|
||||
}
|
||||
|
||||
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||
for (String command : commandReceiver.getCommands()) {
|
||||
commandMap.put(command, commandReceiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
|
||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||
for (String command : commandReceiver.getCommands()) {
|
||||
commandMap.remove(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMediaSessionPlaybackState() {
|
||||
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
||||
if (player == null) {
|
||||
|
|
@ -482,11 +522,8 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
|
||||
private long buildPlaybackActions() {
|
||||
long actions = 0;
|
||||
if (playbackController != null) {
|
||||
actions |= (PlaybackController.ACTIONS & playbackController
|
||||
.getSupportedPlaybackActions(player));
|
||||
}
|
||||
long actions = (PlaybackController.ACTIONS
|
||||
& playbackController.getSupportedPlaybackActions(player));
|
||||
if (playbackPreparer != null) {
|
||||
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
|
||||
}
|
||||
|
|
@ -571,7 +608,7 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
|
||||
private boolean canDispatchToPlaybackController(long action) {
|
||||
return playbackController != null && (playbackController.getSupportedPlaybackActions(player)
|
||||
return (playbackController.getSupportedPlaybackActions(player)
|
||||
& PlaybackController.ACTIONS & action) != 0;
|
||||
}
|
||||
|
||||
|
|
@ -585,17 +622,22 @@ public final class MediaSessionConnector {
|
|||
& QueueEditor.ACTIONS & action) != 0;
|
||||
}
|
||||
|
||||
private class ExoPlayerEventListener implements Player.EventListener {
|
||||
private class ExoPlayerEventListener extends Player.DefaultEventListener {
|
||||
|
||||
private int currentWindowIndex;
|
||||
private int currentWindowCount;
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
int windowCount = player.getCurrentTimeline().getWindowCount();
|
||||
int windowIndex = player.getCurrentWindowIndex();
|
||||
if (queueNavigator != null) {
|
||||
queueNavigator.onTimelineChanged(player);
|
||||
updateMediaSessionPlaybackState();
|
||||
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
|
||||
// active queue item and queue navigation actions may need to be updated
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
int windowCount = player.getCurrentTimeline().getWindowCount();
|
||||
if (currentWindowCount != windowCount) {
|
||||
// active queue item and queue navigation actions may need to be updated
|
||||
updateMediaSessionPlaybackState();
|
||||
|
|
@ -605,16 +647,6 @@ public final class MediaSessionConnector {
|
|||
updateMediaSessionMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
updateMediaSessionPlaybackState();
|
||||
|
|
@ -628,6 +660,13 @@ public final class MediaSessionConnector {
|
|||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
playbackException = error;
|
||||
|
|
@ -635,13 +674,13 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (currentWindowIndex != player.getCurrentWindowIndex()) {
|
||||
if (queueNavigator != null) {
|
||||
queueNavigator.onCurrentWindowIndexChanged(player);
|
||||
}
|
||||
updateMediaSessionMetadata();
|
||||
currentWindowIndex = player.getCurrentWindowIndex();
|
||||
updateMediaSessionMetadata();
|
||||
}
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
|
|
@ -697,6 +736,20 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffleMode(int shuffleMode) {
|
||||
if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
|
||||
playbackController.onSetShuffleMode(player, shuffleMode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRepeatMode(int repeatMode) {
|
||||
if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
|
||||
playbackController.onSetRepeatMode(player, repeatMode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToNext() {
|
||||
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
|
||||
|
|
@ -718,11 +771,6 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRepeatMode(int repeatMode) {
|
||||
// implemented as custom action
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
|
||||
Map<String, CustomActionProvider> actionMap = customActionMap;
|
||||
|
|
@ -734,8 +782,9 @@ public final class MediaSessionConnector {
|
|||
|
||||
@Override
|
||||
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
|
||||
if (playbackPreparer != null) {
|
||||
playbackPreparer.onCommand(command, extras, cb);
|
||||
CommandReceiver commandReceiver = commandMap.get(command);
|
||||
if (commandReceiver != null) {
|
||||
commandReceiver.onCommand(player, command, extras, cb);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -802,13 +851,6 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffleModeEnabled(boolean enabled) {
|
||||
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) {
|
||||
queueNavigator.onSetShuffleModeEnabled(player, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||
if (queueEditor != null) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
package com.google.android.exoplayer2.ext.mediasession;
|
||||
/*
|
||||
* Copyright (c) 2017 The Android Open Source Project
|
||||
*
|
||||
|
|
@ -14,6 +13,7 @@ package com.google.android.exoplayer2.ext.mediasession;
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.mediasession;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -26,12 +26,6 @@ import com.google.android.exoplayer2.util.RepeatModeUtil;
|
|||
*/
|
||||
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
|
||||
|
||||
/**
|
||||
* The default repeat toggle modes.
|
||||
*/
|
||||
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
|
||||
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
|
||||
|
||||
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
|
||||
|
||||
private final Player player;
|
||||
|
|
@ -45,13 +39,13 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
|
|||
* Creates a new instance.
|
||||
* <p>
|
||||
* Equivalent to {@code RepeatModeActionProvider(context, player,
|
||||
* RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}.
|
||||
* MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param player The player on which to toggle the repeat mode.
|
||||
*/
|
||||
public RepeatModeActionProvider(Context context, Player player) {
|
||||
this(context, player, DEFAULT_REPEAT_TOGGLE_MODES);
|
||||
this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Copyright (c) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.mediasession;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.RatingCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link MediaSessionConnector.QueueEditor} implementation based on the
|
||||
* {@link DynamicConcatenatingMediaSource}.
|
||||
* <p>
|
||||
* This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
|
||||
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
|
||||
* This allows to move the currently playing window without interrupting playback.
|
||||
*/
|
||||
public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor,
|
||||
MediaSessionConnector.CommandReceiver {
|
||||
|
||||
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
|
||||
public static final String EXTRA_FROM_INDEX = "from_index";
|
||||
public static final String EXTRA_TO_INDEX = "to_index";
|
||||
|
||||
/**
|
||||
* Factory to create {@link MediaSource}s.
|
||||
*/
|
||||
public interface MediaSourceFactory {
|
||||
/**
|
||||
* Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}.
|
||||
*
|
||||
* @param description The {@link MediaDescriptionCompat} to create a media source for.
|
||||
* @return A {@link MediaSource} or {@code null} if no source can be created for the given
|
||||
* description.
|
||||
*/
|
||||
@Nullable MediaSource createMediaSource(MediaDescriptionCompat description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter to get {@link MediaDescriptionCompat} of items in the queue and to notify the
|
||||
* application about changes in the queue to sync the data structure backing the
|
||||
* {@link MediaSessionConnector}.
|
||||
*/
|
||||
public interface QueueDataAdapter {
|
||||
/**
|
||||
* Gets the {@link MediaDescriptionCompat} for a {@code position}.
|
||||
*
|
||||
* @param position The position in the queue for which to provide a description.
|
||||
* @return A {@link MediaDescriptionCompat}.
|
||||
*/
|
||||
MediaDescriptionCompat getMediaDescription(int position);
|
||||
/**
|
||||
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
|
||||
*
|
||||
* @param position The position at which to add.
|
||||
* @param description The {@link MediaDescriptionCompat} to be added.
|
||||
*/
|
||||
void add(int position, MediaDescriptionCompat description);
|
||||
/**
|
||||
* Removes the item at the given {@code position}.
|
||||
*
|
||||
* @param position The position at which to remove the item.
|
||||
*/
|
||||
void remove(int position);
|
||||
/**
|
||||
* Moves a queue item from position {@code from} to position {@code to}.
|
||||
*
|
||||
* @param from The position from which to remove the item.
|
||||
* @param to The target position to which to move the item.
|
||||
*/
|
||||
void move(int from, int to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to evaluate whether two {@link MediaDescriptionCompat} are considered equal.
|
||||
*/
|
||||
interface MediaDescriptionEqualityChecker {
|
||||
/**
|
||||
* Returns {@code true} whether the descriptions are considered equal.
|
||||
*
|
||||
* @param d1 The first {@link MediaDescriptionCompat}.
|
||||
* @param d2 The second {@link MediaDescriptionCompat}.
|
||||
* @return {@code true} if considered equal.
|
||||
*/
|
||||
boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Media description comparator comparing the media IDs. Media IDs are considered equals if both
|
||||
* are {@code null}.
|
||||
*/
|
||||
public static final class MediaIdEqualityChecker implements MediaDescriptionEqualityChecker {
|
||||
|
||||
@Override
|
||||
public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {
|
||||
return Util.areEqual(d1.getMediaId(), d2.getMediaId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final MediaControllerCompat mediaController;
|
||||
private final QueueDataAdapter queueDataAdapter;
|
||||
private final MediaSourceFactory sourceFactory;
|
||||
private final MediaDescriptionEqualityChecker equalityChecker;
|
||||
private final DynamicConcatenatingMediaSource queueMediaSource;
|
||||
|
||||
/**
|
||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||
*
|
||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
|
||||
* manipulate.
|
||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||
*/
|
||||
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
|
||||
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) {
|
||||
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
|
||||
new MediaIdEqualityChecker());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||
*
|
||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
|
||||
* manipulate.
|
||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
|
||||
*/
|
||||
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
|
||||
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory,
|
||||
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
|
||||
this.mediaController = mediaController;
|
||||
this.queueMediaSource = queueMediaSource;
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
this.sourceFactory = sourceFactory;
|
||||
this.equalityChecker = equalityChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSupportedQueueEditorActions(@Nullable Player player) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(Player player, MediaDescriptionCompat description) {
|
||||
onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
|
||||
MediaSource mediaSource = sourceFactory.createMediaSource(description);
|
||||
if (mediaSource != null) {
|
||||
queueDataAdapter.add(index, description);
|
||||
queueMediaSource.addMediaSource(index, mediaSource);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveQueueItem(Player player, MediaDescriptionCompat description) {
|
||||
List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
|
||||
for (int i = 0; i < queue.size(); i++) {
|
||||
if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
|
||||
onRemoveQueueItemAt(player, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveQueueItemAt(Player player, int index) {
|
||||
queueDataAdapter.remove(index);
|
||||
queueMediaSource.removeMediaSource(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRating(Player player, RatingCompat rating) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// CommandReceiver implementation.
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] getCommands() {
|
||||
return new String[] {COMMAND_MOVE_QUEUE_ITEM};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||
int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);
|
||||
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
|
||||
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
|
||||
queueDataAdapter.move(from, to);
|
||||
queueMediaSource.moveMediaSource(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.mediasession;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
|
@ -23,7 +25,6 @@ import com.google.android.exoplayer2.C;
|
|||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -126,8 +127,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||
if (timeline.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(),
|
||||
player.getRepeatMode());
|
||||
int previousWindowIndex = player.getPreviousWindowIndex();
|
||||
if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|
||||
|| previousWindowIndex == C.INDEX_UNSET) {
|
||||
player.seekTo(0);
|
||||
|
|
@ -154,16 +154,22 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||
if (timeline.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(),
|
||||
player.getRepeatMode());
|
||||
int nextWindowIndex = player.getNextWindowIndex();
|
||||
if (nextWindowIndex != C.INDEX_UNSET) {
|
||||
player.seekTo(nextWindowIndex, C.TIME_UNSET);
|
||||
}
|
||||
}
|
||||
|
||||
// CommandReceiver implementation.
|
||||
|
||||
@Override
|
||||
public void onSetShuffleModeEnabled(Player player, boolean enabled) {
|
||||
// TODO: Implement this.
|
||||
public String[] getCommands() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void publishFloatingQueueWindow(Player player) {
|
||||
|
|
@ -186,3 +192,4 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ The OkHttp extension is an [HttpDataSource][] implementation using Square's
|
|||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||
[OkHttp]: https://square.github.io/okhttp/
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension requires depending on OkHttp, which is
|
||||
licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile('com.squareup.okhttp3:okhttp:3.8.1') {
|
||||
compile('com.squareup.okhttp3:okhttp:3.9.0') {
|
||||
exclude group: 'org.json'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus
|
||||
decoding library) to decode Opus audio.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.opus.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
|
|
|||
|
|
@ -22,15 +22,11 @@ import android.test.InstrumentationTestCase;
|
|||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
/**
|
||||
|
|
@ -45,20 +41,22 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
private void playUri(String uri) throws ExoPlaybackException {
|
||||
TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri),
|
||||
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
|
||||
getInstrumentation().getContext());
|
||||
Thread thread = new Thread(testPlaybackRunnable);
|
||||
thread.start();
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
fail(); // Should never happen.
|
||||
}
|
||||
if (thread.playbackException != null) {
|
||||
throw thread.playbackException;
|
||||
if (testPlaybackRunnable.playbackException != null) {
|
||||
throw testPlaybackRunnable.playbackException;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestPlaybackThread extends Thread implements Player.EventListener {
|
||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
||||
implements Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
|
@ -66,7 +64,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
|||
private ExoPlayer player;
|
||||
private ExoPlaybackException playbackException;
|
||||
|
||||
public TestPlaybackThread(Uri uri, Context context) {
|
||||
public TestPlaybackRunnable(Uri uri, Context context) {
|
||||
this.uri = uri;
|
||||
this.context = context;
|
||||
}
|
||||
|
|
@ -89,31 +87,6 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
|||
Looper.loop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
playbackException = error;
|
||||
|
|
@ -123,20 +96,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
|||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED
|
||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||
releasePlayerAndQuitLooper();
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,9 +71,16 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
|
||||
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
if (!OpusLibrary.isAvailable()
|
||||
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@ streams using [LibRtmp Client for Android][].
|
|||
[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
|
||||
[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension requires depending on LibRtmp Client for
|
||||
Android, which is licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx
|
||||
decoding library) to decode VP9 video.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.vp9.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
|
|
|||
|
|
@ -23,15 +23,11 @@ import android.util.Log;
|
|||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
/**
|
||||
|
|
@ -74,20 +70,22 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
private void playUri(String uri) throws ExoPlaybackException {
|
||||
TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri),
|
||||
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
|
||||
getInstrumentation().getContext());
|
||||
Thread thread = new Thread(testPlaybackRunnable);
|
||||
thread.start();
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
fail(); // Should never happen.
|
||||
}
|
||||
if (thread.playbackException != null) {
|
||||
throw thread.playbackException;
|
||||
if (testPlaybackRunnable.playbackException != null) {
|
||||
throw testPlaybackRunnable.playbackException;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestPlaybackThread extends Thread implements Player.EventListener {
|
||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
||||
implements Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
|
@ -95,7 +93,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
private ExoPlayer player;
|
||||
private ExoPlaybackException playbackException;
|
||||
|
||||
public TestPlaybackThread(Uri uri, Context context) {
|
||||
public TestPlaybackRunnable(Uri uri, Context context) {
|
||||
this.uri = uri;
|
||||
this.context = context;
|
||||
}
|
||||
|
|
@ -121,31 +119,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
Looper.loop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
playbackException = error;
|
||||
|
|
@ -155,20 +128,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED
|
||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||
releasePlayerAndQuitLooper();
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,12 +109,12 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
private DrmSession<ExoMediaCrypto> drmSession;
|
||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||
|
||||
@ReinitializationState
|
||||
private int decoderReinitializationState;
|
||||
private @ReinitializationState int decoderReinitializationState;
|
||||
private boolean decoderReceivedBuffers;
|
||||
|
||||
private Bitmap bitmap;
|
||||
private boolean renderedFirstFrame;
|
||||
private boolean forceRenderFrame;
|
||||
private long joiningDeadlineMs;
|
||||
private Surface surface;
|
||||
private VpxOutputBufferRenderer outputBufferRenderer;
|
||||
|
|
@ -129,6 +129,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
private long droppedFrameAccumulationStartTimeMs;
|
||||
private int droppedFrames;
|
||||
private int consecutiveDroppedFrameCount;
|
||||
private int buffersInCodecCount;
|
||||
|
||||
/**
|
||||
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
|
||||
|
|
@ -194,8 +195,12 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
return VpxLibrary.isAvailable() && MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)
|
||||
? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE;
|
||||
if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
}
|
||||
return FORMAT_HANDLED | ADAPTIVE_SEAMLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -253,6 +258,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
return false;
|
||||
}
|
||||
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
||||
buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
|
||||
}
|
||||
|
||||
if (nextOutputBuffer == null) {
|
||||
|
|
@ -275,26 +281,42 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
|
||||
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
|
||||
if (isBufferLate(outputBuffer.timeUs - positionUs)) {
|
||||
forceRenderFrame = false;
|
||||
skipBuffer();
|
||||
buffersInCodecCount--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (forceRenderFrame) {
|
||||
forceRenderFrame = false;
|
||||
renderBuffer();
|
||||
buffersInCodecCount--;
|
||||
return true;
|
||||
}
|
||||
|
||||
final long nextOutputBufferTimeUs =
|
||||
nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream()
|
||||
? nextOutputBuffer.timeUs : C.TIME_UNSET;
|
||||
if (shouldDropOutputBuffer(
|
||||
|
||||
long earlyUs = outputBuffer.timeUs - positionUs;
|
||||
if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) {
|
||||
forceRenderFrame = true;
|
||||
return false;
|
||||
} else if (shouldDropOutputBuffer(
|
||||
outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) {
|
||||
dropBuffer();
|
||||
buffersInCodecCount--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have yet to render a frame to the current output (either initially or immediately
|
||||
// following a seek), render one irrespective of the state or current position.
|
||||
if (!renderedFirstFrame
|
||||
|| (getState() == STATE_STARTED && outputBuffer.timeUs <= positionUs + 30000)) {
|
||||
|| (getState() == STATE_STARTED && earlyUs <= 30000)) {
|
||||
renderBuffer();
|
||||
buffersInCodecCount--;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -303,18 +325,29 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
* Returns whether the current frame should be dropped.
|
||||
*
|
||||
* @param outputBufferTimeUs The timestamp of the current output buffer.
|
||||
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or
|
||||
* {@link C#TIME_UNSET} if the next output buffer is unavailable.
|
||||
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET}
|
||||
* if the next output buffer is unavailable.
|
||||
* @param positionUs The current playback position.
|
||||
* @param joiningDeadlineMs The joining deadline.
|
||||
* @return Returns whether to drop the current output buffer.
|
||||
*/
|
||||
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
|
||||
private boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
|
||||
long positionUs, long joiningDeadlineMs) {
|
||||
return isBufferLate(outputBufferTimeUs - positionUs)
|
||||
&& (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether to drop all buffers from the buffer being processed to the keyframe at or after
|
||||
* the current playback position, if possible.
|
||||
*
|
||||
* @param earlyUs The time until the current buffer should be presented in microseconds. A
|
||||
* negative value indicates that the buffer is late.
|
||||
*/
|
||||
private boolean shouldDropBuffersToKeyframe(long earlyUs) {
|
||||
return isBufferVeryLate(earlyUs);
|
||||
}
|
||||
|
||||
private void renderBuffer() {
|
||||
int bufferMode = outputBuffer.mode;
|
||||
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
|
||||
|
|
@ -338,18 +371,35 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
}
|
||||
|
||||
private void dropBuffer() {
|
||||
decoderCounters.droppedOutputBufferCount++;
|
||||
droppedFrames++;
|
||||
consecutiveDroppedFrameCount++;
|
||||
decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(
|
||||
consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedOutputBufferCount);
|
||||
if (droppedFrames == maxDroppedFramesToNotify) {
|
||||
maybeNotifyDroppedFrames();
|
||||
}
|
||||
updateDroppedBufferCounters(1);
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
}
|
||||
|
||||
private boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException {
|
||||
int droppedSourceBufferCount = skipSource(positionUs);
|
||||
if (droppedSourceBufferCount == 0) {
|
||||
return false;
|
||||
}
|
||||
decoderCounters.droppedToKeyframeCount++;
|
||||
// We dropped some buffers to catch up, so update the decoder counters and flush the codec,
|
||||
// which releases all pending buffers buffers including the current output buffer.
|
||||
updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount);
|
||||
flushDecoder();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDroppedBufferCounters(int droppedBufferCount) {
|
||||
decoderCounters.droppedBufferCount += droppedBufferCount;
|
||||
droppedFrames += droppedBufferCount;
|
||||
consecutiveDroppedFrameCount += droppedBufferCount;
|
||||
decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount,
|
||||
decoderCounters.maxConsecutiveDroppedBufferCount);
|
||||
if (droppedFrames >= maxDroppedFramesToNotify) {
|
||||
maybeNotifyDroppedFrames();
|
||||
}
|
||||
}
|
||||
|
||||
private void skipBuffer() {
|
||||
decoderCounters.skippedOutputBufferCount++;
|
||||
outputBuffer.release();
|
||||
|
|
@ -422,6 +472,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
inputBuffer.flip();
|
||||
inputBuffer.colorInfo = formatHolder.format.colorInfo;
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
buffersInCodecCount++;
|
||||
decoderReceivedBuffers = true;
|
||||
decoderCounters.inputBufferCount++;
|
||||
inputBuffer = null;
|
||||
|
|
@ -441,6 +492,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
|
||||
private void flushDecoder() throws ExoPlaybackException {
|
||||
waitingForKeys = false;
|
||||
forceRenderFrame = false;
|
||||
buffersInCodecCount = 0;
|
||||
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
|
||||
releaseDecoder();
|
||||
maybeInitDecoder();
|
||||
|
|
@ -597,6 +650,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
decoderCounters.decoderReleaseCount++;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
decoderReceivedBuffers = false;
|
||||
forceRenderFrame = false;
|
||||
buffersInCodecCount = 0;
|
||||
}
|
||||
|
||||
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||
|
|
@ -731,8 +786,13 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
}
|
||||
|
||||
private static boolean isBufferLate(long earlyUs) {
|
||||
// Class a buffer as late if it should have been presented more than 30ms ago.
|
||||
// Class a buffer as late if it should have been presented more than 30 ms ago.
|
||||
return earlyUs < -30000;
|
||||
}
|
||||
|
||||
private static boolean isBufferVeryLate(long earlyUs) {
|
||||
// Class a buffer as very late if it should have been presented more than 500 ms ago.
|
||||
return earlyUs < -500000;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,14 +120,16 @@ import java.nio.ByteBuffer;
|
|||
}
|
||||
}
|
||||
|
||||
outputBuffer.init(inputBuffer.timeUs, outputMode);
|
||||
int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);
|
||||
if (getFrameResult == 1) {
|
||||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
} else if (getFrameResult == -1) {
|
||||
return new VpxDecoderException("Buffer initialization failed.");
|
||||
if (!inputBuffer.isDecodeOnly()) {
|
||||
outputBuffer.init(inputBuffer.timeUs, outputMode);
|
||||
int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);
|
||||
if (getFrameResult == 1) {
|
||||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
} else if (getFrameResult == -1) {
|
||||
return new VpxDecoderException("Buffer initialization failed.");
|
||||
}
|
||||
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||
}
|
||||
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.util.AttributeSet;
|
||||
|
|
@ -23,7 +22,6 @@ import android.util.AttributeSet;
|
|||
/**
|
||||
* A GLSurfaceView extension that scales itself to the given aspect ratio.
|
||||
*/
|
||||
@TargetApi(11)
|
||||
public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBufferRenderer {
|
||||
|
||||
private final VpxRenderer renderer;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
*/
|
||||
|
||||
#include <cpu-features.h>
|
||||
#ifdef __ARM_NEON__
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
#include <jni.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
|
@ -70,6 +73,216 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
#ifdef __ARM_NEON__
|
||||
static int convert_16_to_8_neon(const vpx_image_t* const img, jbyte* const data,
|
||||
const int32_t uvHeight, const int32_t yLength,
|
||||
const int32_t uvLength) {
|
||||
if (!(android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON)) return 0;
|
||||
uint32x2_t lcg_val = vdup_n_u32(random());
|
||||
lcg_val = vset_lane_u32(random(), lcg_val, 1);
|
||||
// LCG values recommended in good ol' "Numerical Recipes"
|
||||
const uint32x2_t LCG_MULT = vdup_n_u32(1664525);
|
||||
const uint32x2_t LCG_INCR = vdup_n_u32(1013904223);
|
||||
|
||||
const uint16_t* srcBase =
|
||||
reinterpret_cast<uint16_t*>(img->planes[VPX_PLANE_Y]);
|
||||
uint8_t* dstBase = reinterpret_cast<uint8_t*>(data);
|
||||
// In units of uint16_t, so /2 from raw stride
|
||||
const int srcStride = img->stride[VPX_PLANE_Y] / 2;
|
||||
const int dstStride = img->stride[VPX_PLANE_Y];
|
||||
|
||||
for (int y = 0; y < img->d_h; y++) {
|
||||
const uint16_t* src = srcBase;
|
||||
uint8_t* dst = dstBase;
|
||||
|
||||
// Each read consumes 4 2-byte samples, but to reduce branches and
|
||||
// random steps we unroll to four rounds, so each loop consumes 16
|
||||
// samples.
|
||||
const int imax = img->d_w & ~15;
|
||||
int i;
|
||||
for (i = 0; i < imax; i += 16) {
|
||||
// Run a round of the RNG.
|
||||
lcg_val = vmla_u32(LCG_INCR, lcg_val, LCG_MULT);
|
||||
|
||||
// The lower two bits of this LCG parameterization are garbage,
|
||||
// leaving streaks on the image. We access the upper bits of each
|
||||
// 16-bit lane by shifting. (We use this both as an 8- and 16-bit
|
||||
// vector, so the choice of which one to keep it as is arbitrary.)
|
||||
uint8x8_t randvec =
|
||||
vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_val), 8));
|
||||
|
||||
// We retrieve the values and shift them so that the bits we'll
|
||||
// shift out (after biasing) are in the upper 8 bits of each 16-bit
|
||||
// lane.
|
||||
uint16x4_t values = vshl_n_u16(vld1_u16(src), 6);
|
||||
src += 4;
|
||||
|
||||
// We add the bias bits in the lower 8 to the shifted values to get
|
||||
// the final values in the upper 8 bits.
|
||||
uint16x4_t added1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
|
||||
// Shifting the randvec bits left by 2 bits, as an 8-bit vector,
|
||||
// should leave us with enough bias to get the needed rounding
|
||||
// operation.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
|
||||
// Retrieve and sum the next 4 pixels.
|
||||
values = vshl_n_u16(vld1_u16(src), 6);
|
||||
src += 4;
|
||||
uint16x4_t added2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
|
||||
// Reinterpret the two added vectors as 8x8, zip them together, and
|
||||
// discard the lower portions.
|
||||
uint8x8_t zipped =
|
||||
vuzp_u8(vreinterpret_u8_u16(added1), vreinterpret_u8_u16(added2))
|
||||
.val[1];
|
||||
vst1_u8(dst, zipped);
|
||||
dst += 8;
|
||||
|
||||
// Run it again with the next two rounds using the remaining
|
||||
// entropy in randvec.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
values = vshl_n_u16(vld1_u16(src), 6);
|
||||
src += 4;
|
||||
added1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
values = vshl_n_u16(vld1_u16(src), 6);
|
||||
src += 4;
|
||||
added2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
zipped = vuzp_u8(vreinterpret_u8_u16(added1), vreinterpret_u8_u16(added2))
|
||||
.val[1];
|
||||
vst1_u8(dst, zipped);
|
||||
dst += 8;
|
||||
}
|
||||
|
||||
uint32_t randval = 0;
|
||||
// For the remaining pixels in each row - usually none, as most
|
||||
// standard sizes are divisible by 32 - convert them "by hand".
|
||||
while (i < img->d_w) {
|
||||
if (!randval) randval = random();
|
||||
dstBase[i] = (srcBase[i] + (randval & 3)) >> 2;
|
||||
i++;
|
||||
randval >>= 2;
|
||||
}
|
||||
|
||||
srcBase += srcStride;
|
||||
dstBase += dstStride;
|
||||
}
|
||||
|
||||
const uint16_t* srcUBase =
|
||||
reinterpret_cast<uint16_t*>(img->planes[VPX_PLANE_U]);
|
||||
const uint16_t* srcVBase =
|
||||
reinterpret_cast<uint16_t*>(img->planes[VPX_PLANE_V]);
|
||||
const int32_t uvWidth = (img->d_w + 1) / 2;
|
||||
uint8_t* dstUBase = reinterpret_cast<uint8_t*>(data + yLength);
|
||||
uint8_t* dstVBase = reinterpret_cast<uint8_t*>(data + yLength + uvLength);
|
||||
const int srcUVStride = img->stride[VPX_PLANE_V] / 2;
|
||||
const int dstUVStride = img->stride[VPX_PLANE_V];
|
||||
|
||||
for (int y = 0; y < uvHeight; y++) {
|
||||
const uint16_t* srcU = srcUBase;
|
||||
const uint16_t* srcV = srcVBase;
|
||||
uint8_t* dstU = dstUBase;
|
||||
uint8_t* dstV = dstVBase;
|
||||
|
||||
// As before, each i++ consumes 4 samples (8 bytes). For simplicity we
|
||||
// don't unroll these loops more than we have to, which is 8 samples.
|
||||
const int imax = uvWidth & ~7;
|
||||
int i;
|
||||
for (i = 0; i < imax; i += 8) {
|
||||
lcg_val = vmla_u32(LCG_INCR, lcg_val, LCG_MULT);
|
||||
uint8x8_t randvec =
|
||||
vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_val), 8));
|
||||
uint16x4_t uVal1 = vqadd_u16(vshl_n_u16(vld1_u16(srcU), 6),
|
||||
vreinterpret_u16_u8(randvec));
|
||||
srcU += 4;
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
uint16x4_t vVal1 = vqadd_u16(vshl_n_u16(vld1_u16(srcV), 6),
|
||||
vreinterpret_u16_u8(randvec));
|
||||
srcV += 4;
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
uint16x4_t uVal2 = vqadd_u16(vshl_n_u16(vld1_u16(srcU), 6),
|
||||
vreinterpret_u16_u8(randvec));
|
||||
srcU += 4;
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
uint16x4_t vVal2 = vqadd_u16(vshl_n_u16(vld1_u16(srcV), 6),
|
||||
vreinterpret_u16_u8(randvec));
|
||||
srcV += 4;
|
||||
vst1_u8(dstU,
|
||||
vuzp_u8(vreinterpret_u8_u16(uVal1), vreinterpret_u8_u16(uVal2))
|
||||
.val[1]);
|
||||
dstU += 8;
|
||||
vst1_u8(dstV,
|
||||
vuzp_u8(vreinterpret_u8_u16(vVal1), vreinterpret_u8_u16(vVal2))
|
||||
.val[1]);
|
||||
dstV += 8;
|
||||
}
|
||||
|
||||
i *= 4;
|
||||
uint32_t randval = 0;
|
||||
while (i < uvWidth) {
|
||||
if (!randval) randval = random();
|
||||
dstUBase[i] = (srcUBase[i] + (randval & 3)) >> 2;
|
||||
randval >>= 2;
|
||||
dstVBase[i] = (srcVBase[i] + (randval & 3)) >> 2;
|
||||
randval >>= 2;
|
||||
i++;
|
||||
}
|
||||
|
||||
srcUBase += srcUVStride;
|
||||
srcVBase += srcUVStride;
|
||||
dstUBase += dstUVStride;
|
||||
dstVBase += dstUVStride;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif // __ARM_NEON__
|
||||
|
||||
static void convert_16_to_8_standard(const vpx_image_t* const img,
|
||||
jbyte* const data, const int32_t uvHeight,
|
||||
const int32_t yLength,
|
||||
const int32_t uvLength) {
|
||||
// Y
|
||||
int sampleY = 0;
|
||||
for (int y = 0; y < img->d_h; y++) {
|
||||
const uint16_t* srcBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y);
|
||||
int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y;
|
||||
for (int x = 0; x < img->d_w; x++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sampleY += *srcBase++;
|
||||
*destBase++ = sampleY >> 2;
|
||||
sampleY = sampleY & 3; // Remainder.
|
||||
}
|
||||
}
|
||||
// UV
|
||||
int sampleU = 0;
|
||||
int sampleV = 0;
|
||||
const int32_t uvWidth = (img->d_w + 1) / 2;
|
||||
for (int y = 0; y < uvHeight; y++) {
|
||||
const uint16_t* srcUBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y);
|
||||
const uint16_t* srcVBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y);
|
||||
int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y;
|
||||
int8_t* destVBase =
|
||||
data + yLength + uvLength + img->stride[VPX_PLANE_V] * y;
|
||||
for (int x = 0; x < uvWidth; x++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sampleU += *srcUBase++;
|
||||
*destUBase++ = sampleU >> 2;
|
||||
sampleU = sampleU & 3; // Remainder.
|
||||
sampleV += *srcVBase++;
|
||||
*destVBase++ = sampleV >> 2;
|
||||
sampleV = sampleV & 3; // Remainder.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, vpxInit) {
|
||||
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
|
||||
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
||||
|
|
@ -201,47 +414,17 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||
// Note: The stride for BT2020 is twice of what we use so this is wasting
|
||||
// memory. The long term goal however is to upload half-float/short so
|
||||
// it's not important to optimize the stride at this time.
|
||||
// Y
|
||||
int sampleY = 0;
|
||||
for (int y = 0; y < img->d_h; y++) {
|
||||
const uint16_t* srcBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y);
|
||||
int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y;
|
||||
for (int x = 0; x < img->d_w; x++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sampleY += *srcBase++;
|
||||
*destBase++ = sampleY >> 2;
|
||||
sampleY = sampleY & 3; // Remainder.
|
||||
}
|
||||
}
|
||||
// UV
|
||||
int sampleU = 0;
|
||||
int sampleV = 0;
|
||||
const int32_t uvWidth = (img->d_w + 1) / 2;
|
||||
for (int y = 0; y < uvHeight; y++) {
|
||||
const uint16_t* srcUBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y);
|
||||
const uint16_t* srcVBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y);
|
||||
int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y;
|
||||
int8_t* destVBase = data + yLength + uvLength
|
||||
+ img->stride[VPX_PLANE_V] * y;
|
||||
for (int x = 0; x < uvWidth; x++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sampleU += *srcUBase++;
|
||||
*destUBase++ = sampleU >> 2;
|
||||
sampleU = sampleU & 3; // Remainder.
|
||||
sampleV += *srcVBase++;
|
||||
*destVBase++ = sampleV >> 2;
|
||||
sampleV = sampleV & 3; // Remainder.
|
||||
}
|
||||
int converted = 0;
|
||||
#ifdef __ARM_NEON__
|
||||
converted = convert_16_to_8_neon(img, data, uvHeight, yLength, uvLength);
|
||||
#endif // __ARM_NEON__
|
||||
if (!converted) {
|
||||
convert_16_to_8_standard(img, data, uvHeight, yLength, uvLength);
|
||||
}
|
||||
} else {
|
||||
// TODO: This copy can be eliminated by using external frame buffers. This
|
||||
// is insignificant for smaller videos but takes ~1.5ms for 1080p clips.
|
||||
// So this should eventually be gotten rid of.
|
||||
// TODO: This copy can be eliminated by using external frame
|
||||
// buffers. This is insignificant for smaller videos but takes ~1.5ms
|
||||
// for 1080p clips. So this should eventually be gotten rid of.
|
||||
memcpy(data, img->planes[VPX_PLANE_Y], yLength);
|
||||
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
|
||||
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
|
||||
|
|
@ -255,9 +438,7 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
|
|||
return env->NewStringUTF(vpx_codec_error(context));
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) {
|
||||
return errorCode;
|
||||
}
|
||||
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; }
|
||||
|
||||
LIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) {
|
||||
// Doesn't support
|
||||
|
|
|
|||
|
|
@ -13,5 +13,4 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2"/>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ android {
|
|||
androidTest {
|
||||
java.srcDirs += "../../testutils/src/main/java/"
|
||||
}
|
||||
test {
|
||||
java.srcDirs += "../../testutils/src/main/java/"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -44,6 +47,10 @@ dependencies {
|
|||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'com.google.truth:truth:' + truthVersion
|
||||
testCompile 'junit:junit:' + junitVersion
|
||||
testCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.core.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
WEBVTT # This comment is allowed
|
||||
|
||||
# First timestamp is missing the 1/1000ths component, but parse anyway.
|
||||
00:00 --> 00:01.234
|
||||
This is the first subtitle.
|
||||
|
||||
02.345 --> 00:03.456
|
||||
This is the second subtitle.
|
||||
|
||||
0.0.0 --> 00:05.678
|
||||
This should be discarded (too many dots).
|
||||
|
||||
00:06.789 --> not-a-timestamp
|
||||
This should be discarded (not a timestamp).
|
||||
|
|
@ -15,20 +15,22 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
|
||||
import com.google.android.exoplayer2.testutil.ActionSchedule;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
|
|
@ -43,67 +45,59 @@ public final class ExoPlayerTest extends TestCase {
|
|||
*/
|
||||
private static final int TIMEOUT_MS = 10000;
|
||||
|
||||
private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null,
|
||||
MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
|
||||
null, null);
|
||||
private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null,
|
||||
MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without
|
||||
* error.
|
||||
*/
|
||||
public void testPlayEmptyTimeline() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = Timeline.EMPTY;
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
FakeRenderer renderer = new FakeRenderer();
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(0);
|
||||
testRunner.assertTimelinesEqual();
|
||||
assertEquals(0, renderer.formatReadCount);
|
||||
assertEquals(0, renderer.bufferReadCount);
|
||||
assertFalse(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes a single period.
|
||||
*/
|
||||
public void testPlaySinglePeriodTimeline() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
Object manifest = new Object();
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setManifest(manifest).setRenderers(renderer)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(0);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
testRunner.assertManifestsEqual(manifest);
|
||||
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes three periods.
|
||||
*/
|
||||
public void testPlayMultiPeriodTimeline() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0));
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(2, playerWrapper.positionDiscontinuityCount);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(2);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertEquals(3, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,16 +105,12 @@ public final class ExoPlayerTest extends TestCase {
|
|||
* source.
|
||||
*/
|
||||
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
||||
final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10));
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT,
|
||||
TEST_AUDIO_FORMAT);
|
||||
|
||||
FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) {
|
||||
final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
|
||||
|
||||
@Override
|
||||
public long getPositionUs() {
|
||||
|
|
@ -143,35 +133,30 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
// Allow playback to end once the final period is playing.
|
||||
return playerWrapper.positionDiscontinuityCount == 2;
|
||||
return videoRenderer.isEnded();
|
||||
}
|
||||
|
||||
};
|
||||
playerWrapper.setup(mediaSource, videoRenderer, audioRenderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(2, playerWrapper.positionDiscontinuityCount);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(videoRenderer, audioRenderer)
|
||||
.setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(2);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertEquals(1, audioRenderer.positionResetCount);
|
||||
assertTrue(videoRenderer.isEnded);
|
||||
assertTrue(audioRenderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
public void testRepreparationGivesFreshSourceInfo() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
|
||||
// Prepare the player with a source with the first manifest and a non-empty timeline
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
Object firstSourceManifest = new Object();
|
||||
playerWrapper.setup(new FakeMediaSource(timeline, firstSourceManifest, TEST_VIDEO_FORMAT),
|
||||
renderer);
|
||||
playerWrapper.blockUntilSourceInfoRefreshed(TIMEOUT_MS);
|
||||
|
||||
// Prepare the player again with a source and a new manifest, which will never be exposed.
|
||||
MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
|
||||
Builder.VIDEO_FORMAT);
|
||||
final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);
|
||||
final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);
|
||||
playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) {
|
||||
MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) {
|
||||
@Override
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
super.prepareSource(player, isTopLevelSource, listener);
|
||||
|
|
@ -185,29 +170,49 @@ public final class ExoPlayerTest extends TestCase {
|
|||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Prepare the player again with a third source.
|
||||
queuedSourceInfoCountDownLatch.await();
|
||||
};
|
||||
Object thirdSourceManifest = new Object();
|
||||
playerWrapper.prepare(new FakeMediaSource(timeline, thirdSourceManifest, TEST_VIDEO_FORMAT));
|
||||
completePreparationCountDownLatch.countDown();
|
||||
|
||||
// Wait for playback to complete.
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
|
||||
MediaSource thirdSource = new FakeMediaSource(timeline, thirdSourceManifest,
|
||||
Builder.VIDEO_FORMAT);
|
||||
|
||||
// Prepare the player with a source with the first manifest and a non-empty timeline. Prepare
|
||||
// the player again with a source and a new manifest, which will never be exposed. Allow the
|
||||
// test thread to prepare the player with a third source, and block the playback thread until
|
||||
// the test thread's call to prepare() has returned.
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation")
|
||||
.waitForTimelineChanged(timeline)
|
||||
.prepareSource(secondSource)
|
||||
.executeRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
queuedSourceInfoCountDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
})
|
||||
.prepareSource(thirdSource)
|
||||
.executeRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
completePreparationCountDownLatch.countDown();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(0);
|
||||
// The first source's preparation completed with a non-empty timeline. When the player was
|
||||
// re-prepared with the second source, it immediately exposed an empty timeline, but the source
|
||||
// info refresh from the second source was suppressed as we re-prepared with the third source.
|
||||
playerWrapper.assertSourceInfosEquals(
|
||||
Pair.create(timeline, firstSourceManifest),
|
||||
Pair.create(Timeline.EMPTY, null),
|
||||
Pair.create(timeline, thirdSourceManifest));
|
||||
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);
|
||||
testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest);
|
||||
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
}
|
||||
|
||||
public void testRepeatModeChanges() throws Exception {
|
||||
|
|
@ -215,49 +220,95 @@ public final class ExoPlayerTest extends TestCase {
|
|||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000));
|
||||
final int[] actionSchedule = { // 0 -> 1
|
||||
Player.REPEAT_MODE_ONE, // 1 -> 1
|
||||
Player.REPEAT_MODE_OFF, // 1 -> 2
|
||||
Player.REPEAT_MODE_ONE, // 2 -> 2
|
||||
Player.REPEAT_MODE_ALL, // 2 -> 0
|
||||
Player.REPEAT_MODE_ONE, // 0 -> 0
|
||||
-1, // 0 -> 0
|
||||
Player.REPEAT_MODE_OFF, // 0 -> 1
|
||||
-1, // 1 -> 2
|
||||
-1 // 2 -> ended
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 2 -> 2
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ALL) // 2 -> 0
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 0 -> 0
|
||||
.waitForPositionDiscontinuity() // 0 -> 0
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 0 -> end
|
||||
.build();
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertTrue(renderer.isEnded);
|
||||
}
|
||||
|
||||
public void testShuffleModeEnabledChanges() throws Exception {
|
||||
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
|
||||
MediaSource[] fakeMediaSources = {
|
||||
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
|
||||
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
|
||||
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)
|
||||
};
|
||||
int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
|
||||
final LinkedList<Integer> windowIndices = new LinkedList<>();
|
||||
final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
|
||||
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false,
|
||||
new FakeShuffleOrder(3), fakeMediaSources);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testShuffleModeEnabled")
|
||||
.setRepeatMode(Player.REPEAT_MODE_ALL).waitForPositionDiscontinuity() // 0 -> 1
|
||||
.setShuffleModeEnabled(true).waitForPositionDiscontinuity() // 1 -> 0
|
||||
.waitForPositionDiscontinuity().waitForPositionDiscontinuity() // 0 -> 2 -> 1
|
||||
.setShuffleModeEnabled(false).setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 -> end
|
||||
.build();
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2);
|
||||
assertTrue(renderer.isEnded);
|
||||
}
|
||||
|
||||
public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
|
||||
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased")
|
||||
.setRepeatMode(Player.REPEAT_MODE_ALL)
|
||||
.waitForPositionDiscontinuity()
|
||||
.seek(0) // Seek with repeat mode set to REPEAT_MODE_ALL.
|
||||
.waitForPositionDiscontinuity()
|
||||
.setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.
|
||||
.build();
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
assertTrue(renderer.isEnded);
|
||||
}
|
||||
|
||||
public void testSeekProcessedCallback() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000));
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback")
|
||||
// Initial seek before timeline preparation finished.
|
||||
.pause().seek(10).waitForPlaybackState(Player.STATE_READY)
|
||||
// Re-seek to same position, start playback and wait until playback reaches second window.
|
||||
.seek(10).play().waitForPositionDiscontinuity()
|
||||
// Seek twice in concession, expecting the first seek to be replaced.
|
||||
.seek(5).seek(60).build();
|
||||
final List<Integer> playbackStatesWhenSeekProcessed = new ArrayList<>();
|
||||
Player.EventListener eventListener = new Player.DefaultEventListener() {
|
||||
private int currentPlaybackState = Player.STATE_IDLE;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ResourceType")
|
||||
public void onPositionDiscontinuity() {
|
||||
super.onPositionDiscontinuity();
|
||||
int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
|
||||
if (actionSchedule[actionIndex] != -1) {
|
||||
player.setRepeatMode(actionSchedule[actionIndex]);
|
||||
}
|
||||
windowIndices.add(player.getCurrentWindowIndex());
|
||||
actionCounter.countDown();
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
currentPlaybackState = playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekProcessed() {
|
||||
playbackStatesWhenSeekProcessed.add(currentPlaybackState);
|
||||
}
|
||||
};
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
playerWrapper.release();
|
||||
assertTrue("Test playback timed out waiting for action schedule to end.", finished);
|
||||
if (playerWrapper.exception != null) {
|
||||
throw playerWrapper.exception;
|
||||
}
|
||||
assertEquals(expectedWindowIndices.length, windowIndices.size());
|
||||
for (int i = 0; i < expectedWindowIndices.length; i++) {
|
||||
assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
|
||||
}
|
||||
assertEquals(9, playerWrapper.positionDiscontinuityCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(3, playbackStatesWhenSeekProcessed.size());
|
||||
assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0));
|
||||
assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(1));
|
||||
assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,23 +33,25 @@ public class TimelineTest extends TestCase {
|
|||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111));
|
||||
TimelineAsserts.assertWindowIds(timeline, 111);
|
||||
TimelineAsserts.assertPeriodCounts(timeline, 1);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
|
||||
C.INDEX_UNSET);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||
}
|
||||
|
||||
public void testMultiPeriodTimeline() {
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111));
|
||||
TimelineAsserts.assertWindowIds(timeline, 111);
|
||||
TimelineAsserts.assertPeriodCounts(timeline, 5);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
|
||||
C.INDEX_UNSET);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
|
||||
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* 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.drm;
|
||||
|
||||
import static com.google.android.exoplayer2.C.PLAYREADY_UUID;
|
||||
import static com.google.android.exoplayer2.C.WIDEVINE_UUID;
|
||||
import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Unit test for {@link DrmInitData}.
|
||||
*/
|
||||
public class DrmInitDataTest extends TestCase {
|
||||
|
||||
private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 2 /* data seed */));
|
||||
private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||
private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 2 /* data seed */));
|
||||
private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4,
|
||||
TestUtil.buildTestData(128, 3 /* data seed */));
|
||||
|
||||
public void testParcelable() {
|
||||
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);
|
||||
|
||||
Parcel parcel = Parcel.obtain();
|
||||
drmInitDataToParcel.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
|
||||
DrmInitData drmInitDataFromParcel = DrmInitData.CREATOR.createFromParcel(parcel);
|
||||
assertEquals(drmInitDataToParcel, drmInitDataFromParcel);
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
|
||||
public void testEquals() {
|
||||
DrmInitData drmInitData = new DrmInitData(DATA_1, DATA_2);
|
||||
|
||||
// Basic non-referential equality test.
|
||||
DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2);
|
||||
assertEquals(drmInitData, testInitData);
|
||||
assertEquals(drmInitData.hashCode(), testInitData.hashCode());
|
||||
|
||||
// Basic non-referential equality test with non-referential scheme data.
|
||||
testInitData = new DrmInitData(DATA_1B, DATA_2B);
|
||||
assertEquals(drmInitData, testInitData);
|
||||
assertEquals(drmInitData.hashCode(), testInitData.hashCode());
|
||||
|
||||
// Passing the scheme data in reverse order shouldn't affect equality.
|
||||
testInitData = new DrmInitData(DATA_2, DATA_1);
|
||||
assertEquals(drmInitData, testInitData);
|
||||
assertEquals(drmInitData.hashCode(), testInitData.hashCode());
|
||||
|
||||
// Ditto.
|
||||
testInitData = new DrmInitData(DATA_2B, DATA_1B);
|
||||
assertEquals(drmInitData, testInitData);
|
||||
assertEquals(drmInitData.hashCode(), testInitData.hashCode());
|
||||
|
||||
// Different number of tuples should affect equality.
|
||||
testInitData = new DrmInitData(DATA_1);
|
||||
MoreAsserts.assertNotEqual(drmInitData, testInitData);
|
||||
|
||||
// Different data in one of the tuples should affect equality.
|
||||
testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL);
|
||||
MoreAsserts.assertNotEqual(drmInitData, testInitData);
|
||||
}
|
||||
|
||||
public void testGet() {
|
||||
// Basic matching.
|
||||
DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2);
|
||||
assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID));
|
||||
assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID));
|
||||
assertNull(testInitData.get(C.UUID_NIL));
|
||||
|
||||
// Basic matching including universal data.
|
||||
testInitData = new DrmInitData(DATA_1, DATA_2, DATA_UNIVERSAL);
|
||||
assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID));
|
||||
assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID));
|
||||
assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL));
|
||||
|
||||
// Passing the scheme data in reverse order shouldn't affect equality.
|
||||
testInitData = new DrmInitData(DATA_UNIVERSAL, DATA_2, DATA_1);
|
||||
assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID));
|
||||
assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID));
|
||||
assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL));
|
||||
|
||||
// Universal data should be returned in the absence of a specific match.
|
||||
testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL);
|
||||
assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID));
|
||||
assertEquals(DATA_UNIVERSAL, testInitData.get(PLAYREADY_UUID));
|
||||
assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL));
|
||||
}
|
||||
|
||||
public void testDuplicateSchemeDataRejected() {
|
||||
try {
|
||||
new DrmInitData(DATA_1, DATA_1);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
try {
|
||||
new DrmInitData(DATA_1, DATA_1B);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
try {
|
||||
new DrmInitData(DATA_1, DATA_2, DATA_1B);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
|
||||
public void testSchemeDataMatches() {
|
||||
assertTrue(DATA_1.matches(WIDEVINE_UUID));
|
||||
assertFalse(DATA_1.matches(PLAYREADY_UUID));
|
||||
assertFalse(DATA_2.matches(C.UUID_NIL));
|
||||
|
||||
assertFalse(DATA_2.matches(WIDEVINE_UUID));
|
||||
assertTrue(DATA_2.matches(PLAYREADY_UUID));
|
||||
assertFalse(DATA_2.matches(C.UUID_NIL));
|
||||
|
||||
assertTrue(DATA_UNIVERSAL.matches(WIDEVINE_UUID));
|
||||
assertTrue(DATA_UNIVERSAL.matches(PLAYREADY_UUID));
|
||||
assertTrue(DATA_UNIVERSAL.matches(C.UUID_NIL));
|
||||
}
|
||||
|
||||
}
|
||||