Allow easier ExoPlayer/Cast integration

This CL adds the fundamental pieces for ExoPlayer/Cast integration and includes a
demo app to showcase this functionality. However, media queues should be supported
in the first release of this extension.

Issue:#2283

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=165576892
This commit is contained in:
aquilescanta 2017-08-17 07:41:10 -07:00 committed by Oliver Woodman
parent 1cfd246cd3
commit 04d76fa8fc
25 changed files with 1534 additions and 2 deletions

View File

@ -22,6 +22,7 @@ project.ext {
buildToolsVersion = '26' buildToolsVersion = '26'
testSupportLibraryVersion = '0.5' testSupportLibraryVersion = '0.5'
supportLibraryVersion = '26.0.1' supportLibraryVersion = '26.0.1'
playServicesLibraryVersion = '11.0.2'
dexmakerVersion = '1.2' dexmakerVersion = '1.2'
mockitoVersion = '1.9.5' mockitoVersion = '1.9.5'
releaseVersion = 'r2.5.1' releaseVersion = 'r2.5.1'

View File

@ -28,6 +28,7 @@ include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac' include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima' include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-mediasession' include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus' include modulePrefix + 'extension-opus'
@ -46,6 +47,7 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') 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-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')

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

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

View File

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

View File

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

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

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

33
extensions/cast/README.md Normal file
View 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

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

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

View File

@ -0,0 +1,649 @@
/*
* 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 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));
}
}
}
}

View File

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

View File

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

View File

@ -22,7 +22,7 @@ dependencies {
// |-- com.android.support:support-v4:25.2.0 // |-- com.android.support:support-v4:25.2.0
compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.android.support:support-v4:' + supportLibraryVersion
compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' 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 project(modulePrefix + 'library')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion

View File

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