Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2
50
README.md
@ -9,34 +9,39 @@ and extend, and can be updated through Play Store application updates.
|
||||
|
||||
## Documentation ##
|
||||
|
||||
* The [developer guide][] provides a wealth of information to help you get
|
||||
started.
|
||||
* The [class reference][] documents the ExoPlayer library classes.
|
||||
* The [developer guide][] provides a wealth of information.
|
||||
* The [class reference][] documents ExoPlayer classes.
|
||||
* The [release notes][] document the major changes in each release.
|
||||
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
||||
developments!
|
||||
|
||||
[developer guide]: https://google.github.io/ExoPlayer/guide.html
|
||||
[class reference]: https://google.github.io/ExoPlayer/doc/reference
|
||||
[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md
|
||||
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
[developer blog]: https://medium.com/google-exoplayer
|
||||
|
||||
## Using ExoPlayer ##
|
||||
|
||||
ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
|
||||
ExoPlayer modules can be obtained from JCenter. It's also possible to clone the
|
||||
repository and depend on the modules locally.
|
||||
|
||||
### Via jCenter ###
|
||||
### From JCenter ###
|
||||
|
||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||
dependency. You need to make sure you have the jcenter repository included in
|
||||
the `build.gradle` file in the root of your project:
|
||||
dependency. You need to make sure you have the JCenter and Google Maven
|
||||
repositories included in the `build.gradle` file in the root of your project:
|
||||
|
||||
```gradle
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://maven.google.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next add a gradle compile dependency to the `build.gradle` file of your app
|
||||
module. The following will add a dependency to the full ExoPlayer library:
|
||||
module. The following will add a dependency to the full library:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:exoplayer:r2.X.X'
|
||||
@ -53,8 +58,8 @@ compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X'
|
||||
compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X'
|
||||
```
|
||||
|
||||
The available modules are listed below. Adding a dependency to the full
|
||||
ExoPlayer library is equivalent to adding dependencies on all of the modules
|
||||
The available library modules are listed below. Adding a dependency to the full
|
||||
library is equivalent to adding dependencies on all of the library modules
|
||||
individually.
|
||||
|
||||
* `exoplayer-core`: Core functionality (required).
|
||||
@ -63,11 +68,16 @@ individually.
|
||||
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
|
||||
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
|
||||
|
||||
For more details, see the project on [Bintray][]. For information about the
|
||||
latest versions, see the [Release notes][].
|
||||
In addition to library modules, ExoPlayer has multiple extension modules that
|
||||
depend on external libraries to provide additional functionality. Some
|
||||
extensions are available from JCenter, whereas others must be built manaully.
|
||||
Browse the [extensions directory] and their individual READMEs for details.
|
||||
|
||||
More information on the library and extension modules that are available from
|
||||
JCenter can be found on [Bintray][].
|
||||
|
||||
[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||
[Bintray]: https://bintray.com/google/exoplayer
|
||||
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
|
||||
### Locally ###
|
||||
|
||||
@ -106,15 +116,9 @@ compile project(':exoplayer-library-ui)
|
||||
|
||||
#### Project branches ####
|
||||
|
||||
* The project has `dev-vX` and `release-vX` branches, where `X` is the major
|
||||
version number.
|
||||
* Most development work happens on the `dev-vX` branch with the highest major
|
||||
version number. Pull requests should normally be made to this branch.
|
||||
* Bug fixes may be submitted to older `dev-vX` branches. When doing this, the
|
||||
same (or an equivalent) fix should also be submitted to all subsequent
|
||||
`dev-vX` branches.
|
||||
* A `release-vX` branch holds the most recent stable release for major version
|
||||
`X`.
|
||||
* Development work happens on the `dev-v2` branch. Pull requests should
|
||||
normally be made to this branch.
|
||||
* The `release-v2` branch holds the most recent release.
|
||||
|
||||
#### Using Android Studio ####
|
||||
|
||||
|
@ -14,6 +14,9 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://maven.google.com"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.3'
|
||||
|
@ -12,16 +12,17 @@
|
||||
// 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'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '25.4.0'
|
||||
supportLibraryVersion = '26.0.1'
|
||||
playServicesLibraryVersion = '11.0.2'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
releaseVersion = 'r2.5.1'
|
||||
|
@ -28,11 +28,13 @@ include modulePrefix + 'extension-ffmpeg'
|
||||
include modulePrefix + 'extension-flac'
|
||||
include modulePrefix + 'extension-gvr'
|
||||
include modulePrefix + 'extension-ima'
|
||||
include modulePrefix + 'extension-cast'
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
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')
|
||||
@ -45,11 +47,13 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten
|
||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
||||
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
|
||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
|
||||
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
||||
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 11 KiB |
4
demos/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# ExoPlayer demos #
|
||||
|
||||
This directory contains applications that demonstrate how to use ExoPlayer.
|
||||
Browse the individual demos and their READMEs to learn more.
|
4
demos/cast/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Cast demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with Google Cast.
|
51
demos/cast/build.gradle
Normal file
@ -0,0 +1,51 @@
|
||||
// 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-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'extension-cast')
|
||||
}
|
43
demos/cast/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?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.castdemo"
|
||||
android:versionCode="0001"
|
||||
android:versionName="0.0.1">
|
||||
|
||||
<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">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTop" android:label="@string/application_name"
|
||||
android:theme="@style/Theme.AppCompat">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility methods and constants for the Cast demo application.
|
||||
*/
|
||||
/* package */ final class CastDemoUtil {
|
||||
|
||||
public static final String MIME_TYPE_DASH = "application/dash+xml";
|
||||
public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl";
|
||||
public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml";
|
||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||
|
||||
/**
|
||||
* The list of samples available in the cast demo app.
|
||||
*/
|
||||
public static final List<Sample> SAMPLES;
|
||||
|
||||
/**
|
||||
* Represents a media sample.
|
||||
*/
|
||||
public static final class Sample {
|
||||
|
||||
/**
|
||||
* The uri from which the media sample is obtained.
|
||||
*/
|
||||
public final String uri;
|
||||
/**
|
||||
* A descriptive name for the sample.
|
||||
*/
|
||||
public final String name;
|
||||
/**
|
||||
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
|
||||
*/
|
||||
public final String type;
|
||||
|
||||
/**
|
||||
* @param uri See {@link #uri}.
|
||||
* @param name See {@link #name}.
|
||||
* @param type See {@link #type}.
|
||||
*/
|
||||
public Sample(String uri, String name, String type) {
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static {
|
||||
// App samples.
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
"DASH (clear,MP4,H264)", MIME_TYPE_DASH));
|
||||
samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
||||
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
|
||||
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
|
||||
MIME_TYPE_VIDEO_MP4));
|
||||
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
|
||||
}
|
||||
|
||||
private CastDemoUtil() {}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ui.PlaybackControlView;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
|
||||
/**
|
||||
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private SimpleExoPlayerView simpleExoPlayerView;
|
||||
private PlaybackControlView castControlView;
|
||||
private PlayerManager playerManager;
|
||||
|
||||
// Activity lifecycle methods.
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
|
||||
simpleExoPlayerView.requestFocus();
|
||||
|
||||
castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view);
|
||||
|
||||
ListView sampleList = (ListView) findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleListAdapter());
|
||||
sampleList.setOnItemClickListener(new SampleClickListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
|
||||
R.id.media_route_menu_item);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if (Util.SDK_INT > 23) {
|
||||
setupPlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if ((Util.SDK_INT <= 23)) {
|
||||
setupPlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (Util.SDK_INT <= 23) {
|
||||
releasePlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (Util.SDK_INT > 23) {
|
||||
releasePlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
// Activity input.
|
||||
|
||||
@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) || playerManager.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void setupPlayerManager() {
|
||||
playerManager = new PlayerManager(simpleExoPlayerView, castControlView,
|
||||
getApplicationContext());
|
||||
}
|
||||
|
||||
private void releasePlayerManager() {
|
||||
playerManager.release();
|
||||
playerManager = null;
|
||||
}
|
||||
|
||||
// User controls.
|
||||
|
||||
private final class SampleListAdapter extends ArrayAdapter<CastDemoUtil.Sample> {
|
||||
|
||||
public SampleListAdapter() {
|
||||
super(getApplicationContext(), android.R.layout.simple_list_item_1, CastDemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
view.setBackgroundColor(Color.WHITE);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SampleClickListener implements AdapterView.OnItemClickListener {
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (parent.getSelectedItemPosition() != position) {
|
||||
CastDemoUtil.Sample currentSample = CastDemoUtil.SAMPLES.get(position);
|
||||
playerManager.setCurrentSample(currentSample, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlaybackControlView;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
|
||||
/**
|
||||
* Manages players for the ExoPlayer/Cast integration app.
|
||||
*/
|
||||
/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener {
|
||||
|
||||
private static final int PLAYBACK_REMOTE = 1;
|
||||
private static final int PLAYBACK_LOCAL = 2;
|
||||
|
||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
|
||||
|
||||
private final SimpleExoPlayerView exoPlayerView;
|
||||
private final PlaybackControlView castControlView;
|
||||
private final CastContext castContext;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
|
||||
private int playbackLocation;
|
||||
private CastDemoUtil.Sample currentSample;
|
||||
|
||||
/**
|
||||
* @param exoPlayerView The {@link SimpleExoPlayerView} for local playback.
|
||||
* @param castControlView The {@link PlaybackControlView} to control remote playback.
|
||||
* @param context A {@link Context}.
|
||||
*/
|
||||
public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView,
|
||||
Context context) {
|
||||
this.exoPlayerView = exoPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
castContext = CastContext.getSharedInstance(context);
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
exoPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
castPlayer = new CastPlayer(castContext);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the given sample at the given position.
|
||||
*
|
||||
* @param currentSample The {@link CastDemoUtil} to play.
|
||||
* @param positionMs The position at which playback should start.
|
||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||
*/
|
||||
public void setCurrentSample(CastDemoUtil.Sample currentSample, long positionMs,
|
||||
boolean playWhenReady) {
|
||||
this.currentSample = currentSample;
|
||||
if (playbackLocation == PLAYBACK_REMOTE) {
|
||||
castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs,
|
||||
playWhenReady);
|
||||
} else /* playbackLocation == PLAYBACK_LOCAL */ {
|
||||
exoPlayer.setPlayWhenReady(playWhenReady);
|
||||
exoPlayer.seekTo(positionMs);
|
||||
exoPlayer.prepare(buildMediaSource(currentSample), true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current
|
||||
* playback location.
|
||||
*
|
||||
* @param event The {@link KeyEvent}.
|
||||
* @return Whether the event was handled by the target view.
|
||||
*/
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (playbackLocation == PLAYBACK_REMOTE) {
|
||||
return castControlView.dispatchKeyEvent(event);
|
||||
} else /* playbackLocation == PLAYBACK_REMOTE */ {
|
||||
return exoPlayerView.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the manager and the players that it holds.
|
||||
*/
|
||||
public void release() {
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
exoPlayerView.setPlayer(null);
|
||||
exoPlayer.release();
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
||||
@Override
|
||||
public void onCastSessionAvailable() {
|
||||
setPlaybackLocation(PLAYBACK_REMOTE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCastSessionUnavailable() {
|
||||
setPlaybackLocation(PLAYBACK_LOCAL);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) {
|
||||
Uri uri = Uri.parse(sample.uri);
|
||||
switch (sample.type) {
|
||||
case CastDemoUtil.MIME_TYPE_SS:
|
||||
return new SsMediaSource(uri, DATA_SOURCE_FACTORY,
|
||||
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
|
||||
case CastDemoUtil.MIME_TYPE_DASH:
|
||||
return new DashMediaSource(uri, DATA_SOURCE_FACTORY,
|
||||
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
|
||||
case CastDemoUtil.MIME_TYPE_HLS:
|
||||
return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null);
|
||||
case CastDemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(),
|
||||
null, null);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + sample.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPlaybackLocation(int playbackLocation) {
|
||||
if (this.playbackLocation == playbackLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// View management.
|
||||
if (playbackLocation == PLAYBACK_LOCAL) {
|
||||
exoPlayerView.setVisibility(View.VISIBLE);
|
||||
castControlView.hide();
|
||||
} else {
|
||||
exoPlayerView.setVisibility(View.GONE);
|
||||
castControlView.show();
|
||||
}
|
||||
|
||||
long playbackPositionMs = 0;
|
||||
boolean playWhenReady = true;
|
||||
if (exoPlayer != null) {
|
||||
playbackPositionMs = exoPlayer.getCurrentPosition();
|
||||
playWhenReady = exoPlayer.getPlayWhenReady();
|
||||
} else if (this.playbackLocation == PLAYBACK_REMOTE) {
|
||||
playbackPositionMs = castPlayer.getCurrentPosition();
|
||||
playWhenReady = castPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
this.playbackLocation = playbackLocation;
|
||||
if (currentSample != null) {
|
||||
setCurrentSample(currentSample, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
41
demos/cast/src/main/res/layout/main_activity.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?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.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
app:repeat_toggle_modes="all|one"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12" />
|
||||
<ListView
|
||||
android:id="@+id/sample_list"
|
||||
android:choiceMode="singleChoice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12" />
|
||||
<com.google.android.exoplayer2.ui.PlaybackControlView
|
||||
android:id="@+id/cast_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:show_timeout="-1"
|
||||
android:layout_weight="2"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
25
demos/cast/src/main/res/menu/menu.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?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.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/media_route_menu_item"
|
||||
android:title="@string/media_route_menu_title"
|
||||
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
BIN
demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
25
demos/cast/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?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">ExoCast Demo</string>
|
||||
|
||||
<string name="media_route_menu_title">ExoCast</string>
|
||||
|
||||
<string name="error_unsupported_drm">DRM scheme not supported by this device.</string>
|
||||
|
||||
</resources>
|
22
demos/cast/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -1,5 +1,5 @@
|
||||
# Demo application #
|
||||
# ExoPlayer main demo #
|
||||
|
||||
This folder contains a demo application that uses ExoPlayer to play a number
|
||||
This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
|
||||
of test streams. It can be used as a starting point or reference project when
|
||||
developing other applications that make use of the ExoPlayer library.
|
@ -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 {
|
||||
@ -39,9 +39,15 @@ android {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
flavorDimensions "extensions"
|
||||
|
||||
productFlavors {
|
||||
noExtensions
|
||||
withExtensions
|
||||
noExtensions {
|
||||
dimension "extensions"
|
||||
}
|
||||
withExtensions {
|
||||
dimension "extensions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
|
||||
<application
|
||||
android:label="@string/application_name"
|
@ -29,7 +29,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
@ -55,10 +55,9 @@ import java.util.Locale;
|
||||
/**
|
||||
* Logs player events using {@link Log}.
|
||||
*/
|
||||
/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener,
|
||||
VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
|
||||
MetadataRenderer.Output {
|
||||
/* package */ final class EventLogger implements Player.EventListener, MetadataOutput,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final int MAX_TIMELINE_ITEM_LINES = 3;
|
||||
@ -100,6 +99,11 @@ import java.util.Locale;
|
||||
Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
Log.d(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
Log.d(TAG, "positionDiscontinuity");
|
||||
@ -205,7 +209,7 @@ import java.util.Locale;
|
||||
Log.d(TAG, "]");
|
||||
}
|
||||
|
||||
// MetadataRenderer.Output
|
||||
// MetadataOutput
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
@ -431,6 +435,8 @@ import java.util.Locale;
|
||||
return "YES";
|
||||
case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
|
||||
return "NO_EXCEEDS_CAPABILITIES";
|
||||
case RendererCapabilities.FORMAT_UNSUPPORTED_DRM:
|
||||
return "NO_UNSUPPORTED_DRM";
|
||||
case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
|
||||
return "NO_UNSUPPORTED_TYPE";
|
||||
case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
|
@ -294,9 +294,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
||||
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
player.addListener(this);
|
||||
player.addListener(eventLogger);
|
||||
player.addMetadataOutput(eventLogger);
|
||||
player.setAudioDebugListener(eventLogger);
|
||||
player.setVideoDebugListener(eventLogger);
|
||||
player.setMetadataOutput(eventLogger);
|
||||
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
@ -501,6 +501,11 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
if (inErrorState) {
|
@ -271,7 +271,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 {
|
BIN
demos/main/src/main/res/drawable-xhdpi/ic_banner.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
demos/main/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/main/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
5
extensions/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# ExoPlayer extensions #
|
||||
|
||||
ExoPlayer extensions are modules that depend on external libraries to provide
|
||||
additional functionality. Browse the individual extensions and their READMEs to
|
||||
learn more.
|
33
extensions/cast/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# ExoPlayer Cast extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The cast extension is a [Player][] implementation that controls playback on a
|
||||
Cast receiver app.
|
||||
|
||||
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.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-cast: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
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Create a `CastPlayer` and use it to integrate Cast into your app using
|
||||
ExoPlayer's common Player interface. You can try the Cast Extension to see how a
|
||||
[PlaybackControlView][] can be used to control playback in a remote receiver app.
|
||||
|
||||
[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html
|
45
extensions/cast/build.gradle
Normal file
@ -0,0 +1,45 @@
|
||||
// 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 14
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Cast extension'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-cast'
|
||||
releaseDescription = 'Cast extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
16
extensions/cast/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?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.cast"/>
|
@ -0,0 +1,660 @@
|
||||
/*
|
||||
* 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.cast;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.cast.framework.CastSession;
|
||||
import com.google.android.gms.cast.framework.SessionManager;
|
||||
import com.google.android.gms.cast.framework.SessionManagerListener;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||
import com.google.android.gms.common.api.CommonStatusCodes;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||
*
|
||||
* <p>Calls to the methods in this class depend on the availability of an underlying cast session.
|
||||
* If no session is available, method calls have no effect. To keep track of the underyling session,
|
||||
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||
* implemented and attached to the player.
|
||||
*
|
||||
* <p>Methods should be called on the application's main thread.
|
||||
*
|
||||
* <p>Known issues:
|
||||
* <ul>
|
||||
* <li>Part of the Cast API is not exposed through this interface. For instance, volume settings
|
||||
* and track selection.</li>
|
||||
* <li> Repeat mode is not working. See [internal: b/64137174].</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class CastPlayer implements Player {
|
||||
|
||||
/**
|
||||
* Listener of changes in the cast session availability.
|
||||
*/
|
||||
public interface SessionAvailabilityListener {
|
||||
|
||||
/**
|
||||
* Called when a cast session becomes available to the player.
|
||||
*/
|
||||
void onCastSessionAvailable();
|
||||
|
||||
/**
|
||||
* Called when the cast session becomes unavailable.
|
||||
*/
|
||||
void onCastSessionUnavailable();
|
||||
|
||||
}
|
||||
|
||||
private static final String TAG = "CastPlayer";
|
||||
|
||||
private static final int RENDERER_COUNT = 3;
|
||||
private static final int RENDERER_INDEX_VIDEO = 0;
|
||||
private static final int RENDERER_INDEX_AUDIO = 1;
|
||||
private static final int RENDERER_INDEX_TEXT = 2;
|
||||
private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
|
||||
private static final TrackGroupArray EMPTY_TRACK_GROUP_ARRAY = new TrackGroupArray();
|
||||
private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
|
||||
new TrackSelectionArray(null, null, null);
|
||||
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
|
||||
|
||||
private final CastContext castContext;
|
||||
private final Timeline.Window window;
|
||||
|
||||
// Result callbacks.
|
||||
private final StatusListener statusListener;
|
||||
private final RepeatModeResultCallback repeatModeResultCallback;
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
|
||||
// Listeners.
|
||||
private final CopyOnWriteArraySet<EventListener> listeners;
|
||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
private RemoteMediaClient remoteMediaClient;
|
||||
private Timeline currentTimeline;
|
||||
private TrackGroupArray currentTrackGroups;
|
||||
private TrackSelectionArray currentTrackSelection;
|
||||
private long lastReportedPositionMs;
|
||||
private long pendingSeekPositionMs;
|
||||
|
||||
/**
|
||||
* @param castContext The context from which the cast session is obtained.
|
||||
*/
|
||||
public CastPlayer(CastContext castContext) {
|
||||
this.castContext = castContext;
|
||||
window = new Timeline.Window();
|
||||
statusListener = new StatusListener();
|
||||
repeatModeResultCallback = new RepeatModeResultCallback();
|
||||
seekResultCallback = new SeekResultCallback();
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
CastSession session = sessionManager.getCurrentCastSession();
|
||||
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
updateInternalState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads media into the receiver app.
|
||||
*
|
||||
* @param title The title of the media sample.
|
||||
* @param url The url from which the media is obtained.
|
||||
* @param contentMimeType The mime type of the content to play.
|
||||
* @param positionMs The position at which the playback should start in milliseconds.
|
||||
* @param playWhenReady Whether the player should start playback as soon as it is ready to do so.
|
||||
*/
|
||||
public void load(String title, String url, String contentMimeType, long positionMs,
|
||||
boolean playWhenReady) {
|
||||
lastReportedPositionMs = 0;
|
||||
if (remoteMediaClient != null) {
|
||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, title);
|
||||
MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setContentType(contentMimeType).setMetadata(movieMetadata).build();
|
||||
remoteMediaClient.load(mediaInfo, playWhenReady, positionMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a cast session is available for playback.
|
||||
*/
|
||||
public boolean isCastSessionAvailable() {
|
||||
return remoteMediaClient != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener for updates on the cast session availability.
|
||||
*
|
||||
* @param listener The {@link SessionAvailabilityListener}.
|
||||
*/
|
||||
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
|
||||
sessionAvailabilityListener = listener;
|
||||
}
|
||||
|
||||
// Player implementation.
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlaybackState() {
|
||||
if (remoteMediaClient == null) {
|
||||
return STATE_IDLE;
|
||||
}
|
||||
int receiverAppStatus = remoteMediaClient.getPlayerState();
|
||||
switch (receiverAppStatus) {
|
||||
case MediaStatus.PLAYER_STATE_BUFFERING:
|
||||
return STATE_BUFFERING;
|
||||
case MediaStatus.PLAYER_STATE_PLAYING:
|
||||
case MediaStatus.PLAYER_STATE_PAUSED:
|
||||
return STATE_READY;
|
||||
case MediaStatus.PLAYER_STATE_IDLE:
|
||||
case MediaStatus.PLAYER_STATE_UNKNOWN:
|
||||
default:
|
||||
return STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
if (remoteMediaClient == null) {
|
||||
return;
|
||||
}
|
||||
if (playWhenReady) {
|
||||
remoteMediaClient.play();
|
||||
} else {
|
||||
remoteMediaClient.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return remoteMediaClient != null && !remoteMediaClient.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToDefaultPosition() {
|
||||
seekTo(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToDefaultPosition(int windowIndex) {
|
||||
seekTo(windowIndex, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(long positionMs) {
|
||||
seekTo(0, positionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int windowIndex, long positionMs) {
|
||||
if (remoteMediaClient != null) {
|
||||
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
|
||||
pendingSeekPositionMs = positionMs;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (remoteMediaClient != null) {
|
||||
remoteMediaClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
castContext.getSessionManager().removeSessionManagerListener(statusListener, CastSession.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererCount() {
|
||||
// We assume there are three renderers: video, audio, and text.
|
||||
return RENDERER_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererType(int index) {
|
||||
switch (index) {
|
||||
case RENDERER_INDEX_VIDEO:
|
||||
return C.TRACK_TYPE_VIDEO;
|
||||
case RENDERER_INDEX_AUDIO:
|
||||
return C.TRACK_TYPE_AUDIO;
|
||||
case RENDERER_INDEX_TEXT:
|
||||
return C.TRACK_TYPE_TEXT;
|
||||
default:
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
int castRepeatMode;
|
||||
switch (repeatMode) {
|
||||
case REPEAT_MODE_ONE:
|
||||
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
|
||||
break;
|
||||
case REPEAT_MODE_ALL:
|
||||
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL;
|
||||
break;
|
||||
case REPEAT_MODE_OFF:
|
||||
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
remoteMediaClient.queueSetRepeatMode(castRepeatMode, null)
|
||||
.setResultCallback(repeatModeResultCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RepeatMode public int getRepeatMode() {
|
||||
if (remoteMediaClient == null) {
|
||||
return REPEAT_MODE_OFF;
|
||||
}
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
if (mediaStatus == null) {
|
||||
// No media session active, yet.
|
||||
return REPEAT_MODE_OFF;
|
||||
}
|
||||
int castRepeatMode = mediaStatus.getQueueRepeatMode();
|
||||
switch (castRepeatMode) {
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
|
||||
return REPEAT_MODE_ONE;
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_ALL:
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
|
||||
return REPEAT_MODE_ALL;
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_OFF:
|
||||
return REPEAT_MODE_OFF;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
||||
// TODO: Support shuffle mode.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getShuffleModeEnabled() {
|
||||
// TODO: Support shuffle mode.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackSelectionArray getCurrentTrackSelections() {
|
||||
return currentTrackSelection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getCurrentTrackGroups() {
|
||||
return currentTrackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline getCurrentTimeline() {
|
||||
return currentTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable public Object getCurrentManifest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentWindowIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return currentTimeline.isEmpty() ? C.TIME_UNSET
|
||||
: currentTimeline.getWindow(0, window).getDurationMs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return remoteMediaClient == null ? lastReportedPositionMs
|
||||
: pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs
|
||||
: remoteMediaClient.getApproximateStreamPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPosition() {
|
||||
return getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferedPercentage() {
|
||||
long position = getBufferedPosition();
|
||||
long duration = getDuration();
|
||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
|
||||
: duration == 0 ? 100
|
||||
: Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrentWindowDynamic() {
|
||||
return !currentTimeline.isEmpty()
|
||||
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrentWindowSeekable() {
|
||||
return !currentTimeline.isEmpty()
|
||||
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlayingAd() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentAdGroupIndex() {
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentAdIndexInAdGroup() {
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentPosition() {
|
||||
return getCurrentPosition();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
|
||||
if (this.remoteMediaClient == remoteMediaClient) {
|
||||
// Do nothing.
|
||||
return;
|
||||
}
|
||||
if (this.remoteMediaClient != null) {
|
||||
this.remoteMediaClient.removeListener(statusListener);
|
||||
this.remoteMediaClient.removeProgressListener(statusListener);
|
||||
}
|
||||
this.remoteMediaClient = remoteMediaClient;
|
||||
if (remoteMediaClient != null) {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionAvailable();
|
||||
}
|
||||
remoteMediaClient.addListener(statusListener);
|
||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||
} else {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MediaStatus getMediaStatus() {
|
||||
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
||||
}
|
||||
|
||||
private @Nullable MediaInfo getMediaInfo() {
|
||||
return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null;
|
||||
}
|
||||
|
||||
private void updateInternalState() {
|
||||
currentTimeline = Timeline.EMPTY;
|
||||
currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
MediaInfo mediaInfo = getMediaInfo();
|
||||
if (mediaInfo == null) {
|
||||
return;
|
||||
}
|
||||
long streamDurationMs = mediaInfo.getStreamDuration();
|
||||
boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION;
|
||||
currentTimeline = new SinglePeriodTimeline(
|
||||
isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable);
|
||||
|
||||
List<MediaTrack> tracks = mediaInfo.getMediaTracks();
|
||||
if (tracks == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null;
|
||||
if (activeTrackIds == null) {
|
||||
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
|
||||
}
|
||||
|
||||
TrackGroup[] trackGroups = new TrackGroup[tracks.size()];
|
||||
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
MediaTrack mediaTrack = tracks.get(i);
|
||||
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
|
||||
|
||||
long id = mediaTrack.getId();
|
||||
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
|
||||
int rendererIndex = getRendererIndexForTrackType(trackType);
|
||||
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
|
||||
&& trackSelections[rendererIndex] == null) {
|
||||
trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
|
||||
}
|
||||
}
|
||||
currentTrackSelection = new TrackSelectionArray(trackSelections);
|
||||
currentTrackGroups = new TrackGroupArray(trackGroups);
|
||||
}
|
||||
|
||||
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
||||
for (long activeTrackId : activeTrackIds) {
|
||||
if (activeTrackId == id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int getRendererIndexForTrackType(int trackType) {
|
||||
return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO
|
||||
: trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO
|
||||
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT
|
||||
: C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private final class StatusListener implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
|
||||
|
||||
// RemoteMediaClient.ProgressListener implementation.
|
||||
|
||||
@Override
|
||||
public void onProgressUpdated(long progressMs, long unusedDurationMs) {
|
||||
lastReportedPositionMs = progressMs;
|
||||
}
|
||||
|
||||
// RemoteMediaClient.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
boolean playWhenReady = getPlayWhenReady();
|
||||
int playbackState = getPlaybackState();
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdated() {
|
||||
updateInternalState();
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
||||
listener.onTimelineChanged(currentTimeline, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueStatusUpdated() {}
|
||||
|
||||
@Override
|
||||
public void onPreloadStatusUpdated() {}
|
||||
|
||||
@Override
|
||||
public void onSendingRemoteMediaRequest() {}
|
||||
|
||||
@Override
|
||||
public void onAdBreakStatusUpdated() {}
|
||||
|
||||
|
||||
// SessionManagerListener implementation.
|
||||
|
||||
@Override
|
||||
public void onSessionStarted(CastSession castSession, String s) {
|
||||
setRemoteMediaClient(castSession.getRemoteMediaClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionResumed(CastSession castSession, boolean b) {
|
||||
setRemoteMediaClient(castSession.getRemoteMediaClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionEnded(CastSession castSession, int i) {
|
||||
setRemoteMediaClient(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionSuspended(CastSession castSession, int i) {
|
||||
setRemoteMediaClient(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionResumeFailed(CastSession castSession, int statusCode) {
|
||||
Log.e(TAG, "Session resume failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionStarting(CastSession castSession) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionStartFailed(CastSession castSession, int statusCode) {
|
||||
Log.e(TAG, "Session start failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionEnding(CastSession castSession) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionResuming(CastSession castSession, String s) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Result callbacks hooks.
|
||||
|
||||
private final class RepeatModeResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode == CommonStatusCodes.SUCCESS) {
|
||||
int repeatMode = getRepeatMode();
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode == CommonStatusCodes.SUCCESS) {
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
} else if (statusCode == CastStatusCodes.REPLACED) {
|
||||
// A seek was executed before this one completed. Do nothing.
|
||||
} else {
|
||||
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.cast;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility methods for ExoPlayer/Cast integration.
|
||||
*/
|
||||
/* package */ final class CastUtils {
|
||||
|
||||
private static final Map<Integer, String> CAST_STATUS_CODE_TO_STRING;
|
||||
|
||||
/**
|
||||
* Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
|
||||
* {@link CastStatusCodes}.
|
||||
*
|
||||
* @param statusCode A Cast API status code.
|
||||
* @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
|
||||
* {@link CastStatusCodes}.
|
||||
*/
|
||||
public static String getLogString(int statusCode) {
|
||||
String description = CAST_STATUS_CODE_TO_STRING.get(statusCode);
|
||||
return description != null ? description : "Unknown.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Format} instance containing all information contained in the given
|
||||
* {@link MediaTrack} object.
|
||||
*
|
||||
* @param mediaTrack The {@link MediaTrack}.
|
||||
* @return The equivalent {@link Format}.
|
||||
*/
|
||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
|
||||
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
|
||||
}
|
||||
|
||||
static {
|
||||
HashMap<Integer, String> statusCodeToString = new HashMap<>();
|
||||
statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_FOUND,
|
||||
"A requested application could not be found.");
|
||||
statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_RUNNING,
|
||||
"A requested application is not currently running.");
|
||||
statusCodeToString.put(CastStatusCodes.AUTHENTICATION_FAILED, "Authentication failure.");
|
||||
statusCodeToString.put(CastStatusCodes.CANCELED, "An in-progress request has been "
|
||||
+ "canceled, most likely because another action has preempted it.");
|
||||
statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_CREATION_FAILED,
|
||||
"The Cast Remote Display service could not be created.");
|
||||
statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_DISCONNECTED,
|
||||
"The Cast Remote Display service was disconnected.");
|
||||
statusCodeToString.put(CastStatusCodes.FAILED, "The in-progress request failed.");
|
||||
statusCodeToString.put(CastStatusCodes.INTERNAL_ERROR, "An internal error has occurred.");
|
||||
statusCodeToString.put(CastStatusCodes.INTERRUPTED,
|
||||
"A blocking call was interrupted while waiting and did not run to completion.");
|
||||
statusCodeToString.put(CastStatusCodes.INVALID_REQUEST, "An invalid request was made.");
|
||||
statusCodeToString.put(CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL, "A message could "
|
||||
+ "not be sent because there is not enough room in the send buffer at this time.");
|
||||
statusCodeToString.put(CastStatusCodes.MESSAGE_TOO_LARGE,
|
||||
"A message could not be sent because it is too large.");
|
||||
statusCodeToString.put(CastStatusCodes.NETWORK_ERROR, "Network I/O error.");
|
||||
statusCodeToString.put(CastStatusCodes.NOT_ALLOWED,
|
||||
"The request was disallowed and could not be completed.");
|
||||
statusCodeToString.put(CastStatusCodes.REPLACED,
|
||||
"The request's progress is no longer being tracked because another request of the same type"
|
||||
+ " has been made before the first request completed.");
|
||||
statusCodeToString.put(CastStatusCodes.SUCCESS, "Success.");
|
||||
statusCodeToString.put(CastStatusCodes.TIMEOUT, "An operation has timed out.");
|
||||
statusCodeToString.put(CastStatusCodes.UNKNOWN_ERROR,
|
||||
"An unknown, unexpected error has occurred.");
|
||||
CAST_STATUS_CODE_TO_STRING = Collections.unmodifiableMap(statusCodeToString);
|
||||
}
|
||||
|
||||
private CastUtils() {}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.cast;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
||||
import com.google.android.gms.cast.framework.CastOptions;
|
||||
import com.google.android.gms.cast.framework.OptionsProvider;
|
||||
import com.google.android.gms.cast.framework.SessionProvider;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A convenience {@link OptionsProvider} to target the default cast receiver app.
|
||||
*/
|
||||
public final class DefaultCastOptionsProvider implements OptionsProvider {
|
||||
|
||||
@Override
|
||||
public CastOptions getCastOptions(Context context) {
|
||||
return new CastOptions.Builder()
|
||||
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
|
||||
.setStopReceiverApplicationWhenEndingSession(true).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
# ExoPlayer Cronet extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
|
||||
|
||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html
|
||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
||||
|
||||
## Build instructions ##
|
||||
@ -22,12 +20,9 @@ and enable the extension:
|
||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
||||
directory of this extension
|
||||
|
||||
* In your `settings.gradle` file, add the following line before the line that
|
||||
applies `core_settings.gradle`:
|
||||
|
||||
```gradle
|
||||
gradle.ext.exoplayerIncludeCronetExtension = true;
|
||||
```
|
||||
* In your `settings.gradle` file, add
|
||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
||||
applies `core_settings.gradle`.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
||||
@ -56,3 +51,10 @@ new DefaultDataSourceFactory(
|
||||
new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */);
|
||||
```
|
||||
respectively.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
@ -22,11 +21,8 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
@ -68,7 +64,6 @@ public final class ByteArrayUploadDataProviderTest {
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.GINGERBREAD)
|
||||
@Test
|
||||
public void testReadPartialBuffer() throws IOException {
|
||||
byte[] firstHalf = Arrays.copyOfRange(TEST_DATA, 0, TEST_DATA.length / 2);
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
@ -1,18 +1,17 @@
|
||||
# ExoPlayer FFmpeg extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The FFmpeg extension is a [Renderer][] implementation that uses FFmpeg to decode
|
||||
audio.
|
||||
|
||||
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
|
||||
The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for
|
||||
decoding and can render audio encoded in a variety of formats.
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
[top level README][]. The extension is not provided via JCenter (see [#2781][]
|
||||
for more information).
|
||||
|
||||
In addition, it's necessary to build the extension's native components as
|
||||
follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -34,7 +33,11 @@ NDK_PATH="<path to Android NDK>"
|
||||
HOST_PLATFORM="linux-x86_64"
|
||||
```
|
||||
|
||||
* Fetch and build FFmpeg. For example, to fetch and build for armeabi-v7a,
|
||||
* Fetch and build FFmpeg. The configuration flags determine which formats will
|
||||
be supported. See the [Supported formats][] page for more details of the
|
||||
available flags.
|
||||
|
||||
For example, to fetch and build for armeabi-v7a,
|
||||
arm64-v8a and x86 on Linux x86_64:
|
||||
|
||||
```
|
||||
@ -103,5 +106,42 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
|
||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
|
||||
```
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Once you've followed the instructions above to check out, build and depend on
|
||||
the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`.
|
||||
How you do this depends on which player API you're using:
|
||||
|
||||
* If you're passing a `DefaultRenderersFactory` to
|
||||
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
|
||||
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
|
||||
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
|
||||
`FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
|
||||
support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
|
||||
`FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`.
|
||||
* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer`
|
||||
to the output list in `buildAudioRenderers`. ExoPlayer will use the first
|
||||
`Renderer` in the list that supports the input media format.
|
||||
* If you've implemented your own `RenderersFactory`, return an
|
||||
`FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
|
||||
first `Renderer` in the returned array that supports the input media format.
|
||||
* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer`
|
||||
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
|
||||
list that supports the input media format.
|
||||
|
||||
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
|
||||
a custom track selector the choice of `Renderer` is up to your implementation,
|
||||
so you need to make sure you are passing an `FfmpegAudioRenderer` to the player,
|
||||
then implement your own logic to use the renderer for a given track.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
||||
[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -1,19 +1,16 @@
|
||||
# ExoPlayer Flac extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The Flac extension is a [Renderer][] implementation that helps you bundle
|
||||
libFLAC (the Flac decoding library) into your app and use it along with
|
||||
ExoPlayer to play Flac audio on Android devices.
|
||||
|
||||
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
|
||||
The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which
|
||||
use libFLAC (the Flac decoding library) to extract and decode FLAC audio.
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
[top level README][].
|
||||
|
||||
In addition, it's necessary to build the extension's native components as
|
||||
follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -46,3 +43,47 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Once you've followed the instructions above to check out, build and depend on
|
||||
the extension, the next step is to tell ExoPlayer to use the extractor and/or
|
||||
renderer.
|
||||
|
||||
### Using `FlacExtractor` ###
|
||||
|
||||
`FlacExtractor` is used via `ExtractorMediaSource`. If you're using
|
||||
`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read
|
||||
`.flac` files. If you're not using `DefaultExtractorsFactory`, return a
|
||||
`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation.
|
||||
|
||||
### Using `LibflacAudioRenderer` ###
|
||||
|
||||
* If you're passing a `DefaultRenderersFactory` to
|
||||
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
|
||||
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
|
||||
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
|
||||
`LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
|
||||
support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
|
||||
`LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`.
|
||||
* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer`
|
||||
to the output list in `buildAudioRenderers`. ExoPlayer will use the first
|
||||
`Renderer` in the list that supports the input media format.
|
||||
* If you've implemented your own `RenderersFactory`, return a
|
||||
`LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
|
||||
first `Renderer` in the returned array that supports the input media format.
|
||||
* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer`
|
||||
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
|
||||
list that supports the input media format.
|
||||
|
||||
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
|
||||
a custom track selector the choice of `Renderer` is up to your implementation,
|
||||
so you need to make sure you are passing an `LibflacAudioRenderer` to the
|
||||
player, then implement your own logic to use the renderer for a given track.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -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"
|
||||
|
@ -132,6 +132,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
|
@ -1,7 +1,5 @@
|
||||
# ExoPlayer GVR extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The GVR extension wraps the [Google VR SDK for Android][]. It provides a
|
||||
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
|
||||
of surround sound and ambisonic soundfields.
|
||||
@ -11,17 +9,7 @@ of surround sound and ambisonic soundfields.
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency. You
|
||||
need to make sure you have the jcenter repository included in the `build.gradle`
|
||||
file in the root of your project:
|
||||
|
||||
```gradle
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
Next, include the following in your module's `build.gradle` file:
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
|
||||
@ -36,9 +24,17 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
|
||||
return a GvrAudioProcessor.
|
||||
* If constructing renderers directly, pass a GvrAudioProcessor to
|
||||
MediaCodecAudioRenderer's constructor.
|
||||
* If using `DefaultRenderersFactory`, override
|
||||
`DefaultRenderersFactory.buildAudioProcessors` to return a
|
||||
`GvrAudioProcessor`.
|
||||
* If constructing renderers directly, pass a `GvrAudioProcessor` to
|
||||
`MediaCodecAudioRenderer`'s constructor.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -1,7 +1,5 @@
|
||||
# ExoPlayer IMA extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The IMA extension is a [MediaSource][] implementation wrapping the
|
||||
[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads
|
||||
alongside content.
|
||||
@ -55,3 +53,10 @@ playback.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -22,7 +22,7 @@ dependencies {
|
||||
// |-- 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
|
||||
|
@ -472,13 +472,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
||||
}
|
||||
if (!imaPlayingAd) {
|
||||
imaPlayingAd = true;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onPlay();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPlay();
|
||||
}
|
||||
} else if (imaPausedInAd) {
|
||||
imaPausedInAd = false;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onResume();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onResume();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -509,8 +509,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
||||
return;
|
||||
}
|
||||
imaPausedInAd = true;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onPause();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPause();
|
||||
}
|
||||
}
|
||||
|
||||
@ -555,8 +555,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
||||
} else if (imaPlayingAd && 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -566,11 +566,16 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -630,8 +635,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
||||
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) {
|
||||
|
@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.ima;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ForwardingTimeline;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
* A {@link Timeline} for sources that have ads.
|
||||
*/
|
||||
/* package */ final class SinglePeriodAdTimeline extends Timeline {
|
||||
/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline {
|
||||
|
||||
private final Timeline contentTimeline;
|
||||
private final long[] adGroupTimesUs;
|
||||
private final int[] adCounts;
|
||||
private final int[] adsLoadedCounts;
|
||||
@ -52,9 +52,9 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts,
|
||||
int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs,
|
||||
long adResumePositionUs) {
|
||||
super(contentTimeline);
|
||||
Assertions.checkState(contentTimeline.getPeriodCount() == 1);
|
||||
Assertions.checkState(contentTimeline.getWindowCount() == 1);
|
||||
this.contentTimeline = contentTimeline;
|
||||
this.adGroupTimesUs = adGroupTimesUs;
|
||||
this.adCounts = adCounts;
|
||||
this.adsLoadedCounts = adsLoadedCounts;
|
||||
@ -63,34 +63,13 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
this.adResumePositionUs = adResumePositionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
contentTimeline.getPeriod(periodIndex, period, setIds);
|
||||
timeline.getPeriod(periodIndex, period, setIds);
|
||||
period.set(period.id, period.uid, period.windowIndex, period.durationUs,
|
||||
period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts,
|
||||
adDurationsUs, adResumePositionUs);
|
||||
return period;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return contentTimeline.getIndexOfPeriod(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
31
extensions/leanback/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# ExoPlayer Leanback extension #
|
||||
|
||||
This [Leanback][] Extension provides a [PlayerAdapter][] implementation for
|
||||
ExoPlayer.
|
||||
|
||||
[PlayerAdapter]: https://developer.android.com/reference/android/support/v17/leanback/media/PlayerAdapter.html
|
||||
[Leanback]: https://developer.android.com/reference/android/support/v17/leanback/package-summary.html
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-leanback:rX.X.X'
|
||||
```
|
||||
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
41
extensions/leanback/build.gradle
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile('com.android.support:leanback-v17:' + supportLibraryVersion)
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Leanback extension for Exoplayer library'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-leanback'
|
||||
releaseDescription = 'Leanback extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
17
extensions/leanback/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.leanback"/>
|
@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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.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.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 updatePlayerRunnable;
|
||||
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||
private boolean initialized;
|
||||
private boolean hasSurface;
|
||||
private boolean isBuffering;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
updatePlayerRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Callback callback = getCallback();
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
||||
handler.postDelayed(this, updatePeriodMs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToHost(PlaybackGlueHost host) {
|
||||
if (host instanceof SurfaceHolderGlueHost) {
|
||||
surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);
|
||||
}
|
||||
notifyListeners();
|
||||
player.addListener(componentListener);
|
||||
player.addVideoListener(componentListener);
|
||||
}
|
||||
|
||||
private void notifyListeners() {
|
||||
boolean oldIsPrepared = isPrepared();
|
||||
int playbackState = player.getPlaybackState();
|
||||
boolean isInitialized = playbackState != Player.STATE_IDLE;
|
||||
isBuffering = playbackState == Player.STATE_BUFFERING;
|
||||
boolean hasEnded = playbackState == Player.STATE_ENDED;
|
||||
|
||||
initialized = isInitialized;
|
||||
Callback callback = getCallback();
|
||||
if (oldIsPrepared != isPrepared()) {
|
||||
callback.onPreparedStateChanged(this);
|
||||
}
|
||||
callback.onPlayStateChanged(this);
|
||||
callback.onBufferingStateChanged(this, isBuffering || !initialized);
|
||||
if (hasEnded) {
|
||||
callback.onPlayCompleted(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional {@link ErrorMessageProvider}.
|
||||
*
|
||||
* @param errorMessageProvider The {@link ErrorMessageProvider}.
|
||||
*/
|
||||
public void setErrorMessageProvider(
|
||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
this.errorMessageProvider = errorMessageProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromHost() {
|
||||
player.removeListener(componentListener);
|
||||
player.removeVideoListener(componentListener);
|
||||
if (surfaceHolderGlueHost != null) {
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
||||
surfaceHolderGlueHost = null;
|
||||
}
|
||||
initialized = false;
|
||||
hasSurface = false;
|
||||
Callback callback = getCallback();
|
||||
callback.onBufferingStateChanged(this, false);
|
||||
callback.onPlayStateChanged(this);
|
||||
callback.onPreparedStateChanged(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProgressUpdatingEnabled(final boolean enabled) {
|
||||
handler.removeCallbacks(updatePlayerRunnable);
|
||||
if (enabled) {
|
||||
handler.post(updatePlayerRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return initialized && player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
long durationMs = player.getDuration();
|
||||
return durationMs != C.TIME_UNSET ? durationMs : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return initialized ? player.getCurrentPosition() : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
player.seekToDefaultPosition();
|
||||
}
|
||||
player.setPlayWhenReady(true);
|
||||
getCallback().onPlayStateChanged(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
player.setPlayWhenReady(false);
|
||||
getCallback().onPlayStateChanged(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(long positionMs) {
|
||||
player.seekTo(positionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPosition() {
|
||||
return player.getBufferedPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPrepared() {
|
||||
return initialized && (surfaceHolderGlueHost == null || hasSurface);
|
||||
}
|
||||
|
||||
private void setVideoSurface(Surface surface) {
|
||||
hasSurface = surface != null;
|
||||
player.setVideoSurface(surface);
|
||||
getCallback().onPreparedStateChanged(this);
|
||||
}
|
||||
|
||||
private final class ComponentListener implements Player.EventListener,
|
||||
SimpleExoPlayer.VideoListener, SurfaceHolder.Callback {
|
||||
|
||||
// SurfaceHolder.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder surfaceHolder) {
|
||||
setVideoSurface(surfaceHolder.getSurface());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
|
||||
setVideoSurface(null);
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@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 onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@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 onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
Callback callback = getCallback();
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters params) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// 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.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
# ExoPlayer MediaSession extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The MediaSession extension mediates between an ExoPlayer instance and a
|
||||
[MediaSession][]. It automatically retrieves and implements playback actions
|
||||
and syncs the player state with the state of the media session. The behaviour
|
||||
can be extended to support other playback and custom actions.
|
||||
The MediaSession extension mediates between a Player (or ExoPlayer) instance
|
||||
and a [MediaSession][]. It automatically retrieves and implements playback
|
||||
actions and syncs the player state with the state of the media session. The
|
||||
behaviour can be extended to support other playback and custom actions.
|
||||
|
||||
[MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html
|
||||
|
||||
@ -25,3 +23,10 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching
|
||||
`com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -38,6 +38,7 @@ 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 java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -47,7 +48,7 @@ import java.util.Map;
|
||||
* Connects a {@link MediaSessionCompat} to a {@link Player}.
|
||||
* <p>
|
||||
* The connector listens for actions sent by the media session's controller and implements these
|
||||
* actions by calling appropriate ExoPlayer methods. The playback state of the media session is
|
||||
* actions by calling appropriate player methods. The playback state of the media session is
|
||||
* automatically synced with the player. The connector can also be optionally extended by providing
|
||||
* various collaborators:
|
||||
* <ul>
|
||||
@ -73,6 +74,10 @@ public final class MediaSessionConnector {
|
||||
}
|
||||
|
||||
public static final String EXTRAS_PITCH = "EXO_PITCH";
|
||||
private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
|
||||
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
|
||||
private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
|
||||
| MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
|
||||
|
||||
/**
|
||||
* Interface to which playback preparation actions are delegated.
|
||||
@ -293,17 +298,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}.
|
||||
*/
|
||||
@ -318,9 +312,8 @@ public final class MediaSessionConnector {
|
||||
|
||||
private Player player;
|
||||
private CustomActionProvider[] customActionProviders;
|
||||
private int currentWindowIndex;
|
||||
private Map<String, CustomActionProvider> customActionMap;
|
||||
private ErrorMessageProvider errorMessageProvider;
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private PlaybackPreparer playbackPreparer;
|
||||
private QueueNavigator queueNavigator;
|
||||
private QueueEditor queueEditor;
|
||||
@ -369,8 +362,7 @@ public final class MediaSessionConnector {
|
||||
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
|
||||
: Looper.getMainLooper());
|
||||
this.doMaintainMetadata = doMaintainMetadata;
|
||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
|
||||
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
|
||||
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
|
||||
mediaController = mediaSession.getController();
|
||||
mediaSessionCallback = new MediaSessionCallback();
|
||||
exoPlayerEventListener = new ExoPlayerEventListener();
|
||||
@ -411,7 +403,8 @@ 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;
|
||||
}
|
||||
|
||||
@ -433,6 +426,8 @@ public final class MediaSessionConnector {
|
||||
*/
|
||||
public void setQueueEditor(QueueEditor queueEditor) {
|
||||
this.queueEditor = queueEditor;
|
||||
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
|
||||
: EDITOR_MEDIA_SESSION_FLAGS);
|
||||
}
|
||||
|
||||
private void updateMediaSessionPlaybackState() {
|
||||
@ -583,11 +578,20 @@ public final class MediaSessionConnector {
|
||||
|
||||
private class ExoPlayerEventListener implements Player.EventListener {
|
||||
|
||||
private int currentWindowIndex;
|
||||
private int currentWindowCount;
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
if (queueNavigator != null) {
|
||||
queueNavigator.onTimelineChanged(player);
|
||||
}
|
||||
int windowCount = player.getCurrentTimeline().getWindowCount();
|
||||
if (currentWindowCount != windowCount) {
|
||||
// active queue item and queue navigation actions may need to be updated
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
currentWindowCount = windowCount;
|
||||
currentWindowIndex = player.getCurrentWindowIndex();
|
||||
updateMediaSessionMetadata();
|
||||
}
|
||||
@ -615,6 +619,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;
|
||||
@ -796,6 +807,14 @@ public final class MediaSessionConnector {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffleMode(int shuffleMode) {
|
||||
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) {
|
||||
queueNavigator.onSetShuffleModeEnabled(player,
|
||||
shuffleMode != PlaybackStateCompat.SHUFFLE_MODE_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||
if (queueEditor != null) {
|
||||
|
@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession;
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
@ -163,7 +163,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
||||
|
||||
@Override
|
||||
public void onSetShuffleModeEnabled(Player player, boolean enabled) {
|
||||
// TODO: Implement this.
|
||||
player.setShuffleModeEnabled(enabled);
|
||||
}
|
||||
|
||||
private void publishFloatingQueueWindow(Player player) {
|
||||
|
@ -1,7 +1,5 @@
|
||||
# ExoPlayer OkHttp extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The OkHttp extension is an [HttpDataSource][] implementation using Square's
|
||||
[OkHttp][].
|
||||
|
||||
@ -49,3 +47,10 @@ new DefaultDataSourceFactory(
|
||||
new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */);
|
||||
```
|
||||
respectively.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -1,19 +1,16 @@
|
||||
# ExoPlayer Opus extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The Opus extension is a [Renderer][] implementation that helps you bundle
|
||||
libopus (the Opus decoding library) into your app and use it along with
|
||||
ExoPlayer to play Opus audio on Android devices.
|
||||
|
||||
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
|
||||
The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus
|
||||
decoding library) to decode Opus audio.
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
[top level README][].
|
||||
|
||||
In addition, it's necessary to build the extension's native components as
|
||||
follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -59,3 +56,38 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
* Clean and re-build the project.
|
||||
* If you want to use your own version of libopus, place it in
|
||||
`${OPUS_EXT_PATH}/jni/libopus`.
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Once you've followed the instructions above to check out, build and depend on
|
||||
the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`.
|
||||
How you do this depends on which player API you're using:
|
||||
|
||||
* If you're passing a `DefaultRenderersFactory` to
|
||||
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
|
||||
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
|
||||
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
|
||||
`LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
|
||||
support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
|
||||
`LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`.
|
||||
* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer`
|
||||
to the output list in `buildAudioRenderers`. ExoPlayer will use the first
|
||||
`Renderer` in the list that supports the input media format.
|
||||
* If you've implemented your own `RenderersFactory`, return a
|
||||
`LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
|
||||
first `Renderer` in the returned array that supports the input media format.
|
||||
* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer`
|
||||
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
|
||||
list that supports the input media format.
|
||||
|
||||
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
|
||||
a custom track selector the choice of `Renderer` is up to your implementation,
|
||||
so you need to make sure you are passing an `LibopusAudioRenderer` to the
|
||||
player, then implement your own logic to use the renderer for a given track.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -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"
|
||||
|
@ -132,6 +132,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
|
@ -1,7 +1,5 @@
|
||||
# ExoPlayer RTMP extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The RTMP extension is a [DataSource][] implementation for playing [RTMP][]
|
||||
streams using [LibRtmp Client for Android][].
|
||||
|
||||
@ -9,7 +7,7 @@ streams using [LibRtmp Client for Android][].
|
||||
[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
|
||||
[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android
|
||||
|
||||
## Using the extension ##
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
@ -25,3 +23,26 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
ExoPlayer requests data through `DataSource` instances. These instances are
|
||||
either instantiated and injected from application code, or obtained from
|
||||
instances of `DataSource.Factory` that are instantiated and injected from
|
||||
application code.
|
||||
|
||||
`DefaultDataSource` will automatically use uses the RTMP extension whenever it's
|
||||
available. Hence if your application is using `DefaultDataSource` or
|
||||
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
|
||||
adding a dependency to the RTMP extension as described above. No changes to your
|
||||
application code are required. Alternatively, if you know that your application
|
||||
doesn't need to handle any other protocols, you can update any `DataSource`s and
|
||||
`DataSource.Factory` instantiations in your application code to use
|
||||
`RtmpDataSource` and `RtmpDataSourceFactory` directly.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -1,19 +1,16 @@
|
||||
# ExoPlayer VP9 extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx
|
||||
(the VP9 decoding library) into your app and use it along with ExoPlayer to play
|
||||
VP9 video on Android devices.
|
||||
|
||||
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
|
||||
The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx
|
||||
decoding library) to decode VP9 video.
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][]. In addition, it's necessary to build the extension's
|
||||
native components as follows:
|
||||
[top level README][].
|
||||
|
||||
In addition, it's necessary to build the extension's native components as
|
||||
follows:
|
||||
|
||||
* Set the following environment variables:
|
||||
|
||||
@ -76,3 +73,45 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
|
||||
please note that `generate_libvpx_android_configs.sh` and the makefiles need
|
||||
to be modified to work with arbitrary versions of libvpx and libyuv.
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Once you've followed the instructions above to check out, build and depend on
|
||||
the extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`.
|
||||
How you do this depends on which player API you're using:
|
||||
|
||||
* If you're passing a `DefaultRenderersFactory` to
|
||||
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
|
||||
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
|
||||
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
|
||||
`LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't
|
||||
support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER`
|
||||
to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`.
|
||||
* If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer`
|
||||
to the output list in `buildVideoRenderers`. ExoPlayer will use the first
|
||||
`Renderer` in the list that supports the input media format.
|
||||
* If you've implemented your own `RenderersFactory`, return a
|
||||
`LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the
|
||||
first `Renderer` in the returned array that supports the input media format.
|
||||
* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer`
|
||||
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
|
||||
list that supports the input media format.
|
||||
|
||||
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
|
||||
a custom track selector the choice of `Renderer` is up to your implementation,
|
||||
so you need to make sure you are passing an `LibvpxVideoRenderer` to the
|
||||
player, then implement your own logic to use the renderer for a given track.
|
||||
|
||||
`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not
|
||||
being used via `SimpleExoPlayer`, in which case color space conversion will be
|
||||
performed using a GL shader. To enable this mode, send the renderer a message of
|
||||
type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the
|
||||
`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a
|
||||
`Surface`.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*`
|
||||
belong to this module.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
||||
|
@ -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"
|
||||
|
@ -164,6 +164,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
|
@ -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;
|
||||
|
7
library/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# ExoPlayer library #
|
||||
|
||||
The ExoPlayer library is split into multiple modules. See ExoPlayer's
|
||||
[top level README][] for more information about the available library modules
|
||||
and how to use them.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
13
library/all/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# ExoPlayer full library #
|
||||
|
||||
An empty module that depends on all of the other library modules. Depending on
|
||||
the full library is equivalent to depending on all of the other library modules
|
||||
individually. See ExoPlayer's [top level README][] for more information.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
@ -13,5 +13,4 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2"/>
|
||||
|
9
library/core/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# ExoPlayer core library module #
|
||||
|
||||
The core of the ExoPlayer library.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
|
||||
|
||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.core.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="24"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -0,0 +1,10 @@
|
||||
1
|
||||
00:00:00,000 --> 00:00:01,234
|
||||
This is the first subtitle.
|
||||
|
||||
2
|
||||
00:00:02,345 --> 00:00:03,456
|
||||
This is the second subtitle.
|
||||
Second subtitle with second line.
|
||||
|
||||
3
|
@ -15,20 +15,18 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.util.Pair;
|
||||
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.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
@ -43,67 +41,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(timeline);
|
||||
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 +101,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 +129,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 +166,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 +216,22 @@ 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
|
||||
};
|
||||
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() {
|
||||
@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();
|
||||
}
|
||||
};
|
||||
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);
|
||||
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);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
|
@ -16,7 +16,6 @@
|
||||
package com.google.android.exoplayer2.extractor.mp4;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
@ -38,11 +37,6 @@ public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
|
||||
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testAtomWithZeroSize() throws Exception {
|
||||
ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||
getInstrumentation(), ParserException.class);
|
||||
}
|
||||
|
||||
private static ExtractorFactory getExtractorFactory() {
|
||||
return getExtractorFactory(0);
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.mp4;
|
||||
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.util.UUID;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Tests for {@link PsshAtomUtil}.
|
||||
*/
|
||||
public class PsshAtomUtilTest extends TestCase {
|
||||
|
||||
public void testBuildPsshAtom() {
|
||||
byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5};
|
||||
byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData);
|
||||
// Read the PSSH atom back and assert its content is as expected.
|
||||
ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom);
|
||||
assertEquals(psshAtom.length, parsablePsshAtom.readUnsignedIntToInt()); // length
|
||||
assertEquals(Atom.TYPE_pssh, parsablePsshAtom.readInt()); // type
|
||||
int fullAtomInt = parsablePsshAtom.readInt(); // version + flags
|
||||
assertEquals(0, Atom.parseFullAtomVersion(fullAtomInt));
|
||||
assertEquals(0, Atom.parseFullAtomFlags(fullAtomInt));
|
||||
UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong());
|
||||
assertEquals(C.WIDEVINE_UUID, systemId);
|
||||
assertEquals(schemeData.length, parsablePsshAtom.readUnsignedIntToInt());
|
||||
byte[] psshSchemeData = new byte[schemeData.length];
|
||||
parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length);
|
||||
MoreAsserts.assertEquals(schemeData, psshSchemeData);
|
||||
}
|
||||
|
||||
}
|
@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
||||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(4000, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
new byte[] {'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
||||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(2046, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
new byte[] {'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
||||
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
|
||||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
new byte[]{'x', 'O', 'g', 'g', 'S'}
|
||||
new byte[] {'x', 'O', 'g', 'g', 'S'}
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(1, extractorInput.getPosition());
|
||||
@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
||||
|
||||
public void testSkipToNextPageNoMatch() throws Exception {
|
||||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false);
|
||||
new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
|
||||
try {
|
||||
skipToNextPage(extractorInput);
|
||||
fail();
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
@ -154,20 +155,20 @@ public class AdtsReaderTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public void testAdtsDataOnly() throws Exception {
|
||||
public void testAdtsDataOnly() throws ParserException {
|
||||
data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length);
|
||||
feed();
|
||||
assertSampleCounts(0, 1);
|
||||
adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null);
|
||||
}
|
||||
|
||||
private void feedLimited(int limit) {
|
||||
private void feedLimited(int limit) throws ParserException {
|
||||
maybeStartPacket();
|
||||
data.setLimit(limit);
|
||||
feed();
|
||||
}
|
||||
|
||||
private void feed() {
|
||||
private void feed() throws ParserException {
|
||||
maybeStartPacket();
|
||||
adtsReader.consume(data);
|
||||
}
|
||||
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.offline;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DummyDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProgressiveDownloadAction}.
|
||||
*/
|
||||
public class ProgressiveDownloadActionTest extends InstrumentationTestCase {
|
||||
|
||||
public void testDownloadActionIsNotRemoveAction() throws Exception {
|
||||
ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
|
||||
assertFalse(action.isRemoveAction());
|
||||
}
|
||||
|
||||
public void testRemoveActionIsRemoveAction() throws Exception {
|
||||
ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true);
|
||||
assertTrue(action2.isRemoveAction());
|
||||
}
|
||||
|
||||
public void testCreateDownloader() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
|
||||
DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(
|
||||
Mockito.mock(Cache.class), DummyDataSource.FACTORY);
|
||||
assertNotNull(action.createDownloader(constructorHelper));
|
||||
}
|
||||
|
||||
public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true);
|
||||
ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false);
|
||||
assertTrue(action1.isSameMedia(action2));
|
||||
}
|
||||
|
||||
public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true);
|
||||
ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false);
|
||||
assertFalse(action3.isSameMedia(action4));
|
||||
}
|
||||
|
||||
public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true);
|
||||
ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false);
|
||||
assertTrue(action5.isSameMedia(action6));
|
||||
}
|
||||
|
||||
public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true);
|
||||
ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false);
|
||||
assertFalse(action7.isSameMedia(action8));
|
||||
}
|
||||
|
||||
public void testEquals() throws Exception {
|
||||
ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true);
|
||||
assertTrue(action1.equals(action1));
|
||||
|
||||
ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true);
|
||||
ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true);
|
||||
assertTrue(action2.equals(action3));
|
||||
|
||||
ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true);
|
||||
ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false);
|
||||
assertFalse(action4.equals(action5));
|
||||
|
||||
ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true);
|
||||
ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true);
|
||||
assertFalse(action6.equals(action7));
|
||||
|
||||
ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true);
|
||||
ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true);
|
||||
assertFalse(action8.equals(action9));
|
||||
|
||||
ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true);
|
||||
ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true);
|
||||
assertFalse(action10.equals(action11));
|
||||
}
|
||||
|
||||
public void testSerializerGetType() throws Exception {
|
||||
ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
|
||||
assertNotNull(action.getType());
|
||||
}
|
||||
|
||||
public void testSerializerWriteRead() throws Exception {
|
||||
doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false));
|
||||
doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true));
|
||||
}
|
||||
|
||||
private void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DataOutputStream output = new DataOutputStream(out);
|
||||
action1.writeToStream(output);
|
||||
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
DataInputStream input = new DataInputStream(in);
|
||||
DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input);
|
||||
|
||||
assertEquals(action1, action2);
|
||||
}
|
||||
|
||||
}
|
@ -396,6 +396,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getShuffleModeEnabled() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoading() {
|
||||
throw new UnsupportedOperationException();
|
||||
|