Merge pull request #3493 from google/dev-v2-r2.6.0

r2.6.0
This commit is contained in:
ojw28 2017-11-23 17:22:35 +00:00 committed by GitHub
commit e7c60a2a23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
491 changed files with 21238 additions and 7188 deletions

1
.gitignore vendored
View file

@ -39,6 +39,7 @@ proguard-project.txt
# Other
.DS_Store
cmake-build-debug
dist
tmp

71
.hgignore Normal file
View 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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

4
demos/README.md Normal file
View 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
View 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
View 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')
}

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

View file

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

View file

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

View 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"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

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

View file

@ -27,6 +27,11 @@ android {
sourceSets.main {
jniLibs.srcDirs = ['jniLibs']
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {

View file

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

View file

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

View file

@ -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() {

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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'

View 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"/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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