diff --git a/constants.gradle b/constants.gradle index 576091f937..4107faab4c 100644 --- a/constants.gradle +++ b/constants.gradle @@ -22,6 +22,7 @@ project.ext { buildToolsVersion = '26' testSupportLibraryVersion = '0.5' supportLibraryVersion = '26.0.1' + playServicesLibraryVersion = '11.0.2' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' releaseVersion = 'r2.5.1' diff --git a/core_settings.gradle b/core_settings.gradle index 7a8320b1a1..20a7c87bde 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -28,6 +28,7 @@ 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' @@ -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-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') diff --git a/demos/cast/README.md b/demos/cast/README.md new file mode 100644 index 0000000000..2c68a5277a --- /dev/null +++ b/demos/cast/README.md @@ -0,0 +1,4 @@ +# Cast demo application # + +This folder contains a demo application that showcases ExoPlayer integration +with Google Cast. diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle new file mode 100644 index 0000000000..a9fa27ad58 --- /dev/null +++ b/demos/cast/build.gradle @@ -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') +} diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..eeb28438bd --- /dev/null +++ b/demos/cast/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java new file mode 100644 index 0000000000..f819e54e50 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java @@ -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 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 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() {} + +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java new file mode 100644 index 0000000000..e1367858aa --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -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 { + + 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); + } + } + + } + +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java new file mode 100644 index 0000000000..741df7eff1 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -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); + } + } + +} diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000000..7e39320e3b --- /dev/null +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/demos/cast/src/main/res/menu/menu.xml b/demos/cast/src/main/res/menu/menu.xml new file mode 100644 index 0000000000..075ad34ec4 --- /dev/null +++ b/demos/cast/src/main/res/menu/menu.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..52e8dc93d9 Binary files /dev/null and b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..b55576eff3 Binary files /dev/null and b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..ca84d6a60e Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..27ab9b1054 Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..d1eb9b78cf Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml new file mode 100644 index 0000000000..503892da27 --- /dev/null +++ b/demos/cast/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + + + ExoCast Demo + + ExoCast + + DRM scheme not supported by this device. + + diff --git a/demos/cast/src/main/res/values/styles.xml b/demos/cast/src/main/res/values/styles.xml new file mode 100644 index 0000000000..1484a68a68 --- /dev/null +++ b/demos/cast/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/extensions/cast/README.md b/extensions/cast/README.md new file mode 100644 index 0000000000..73f7041729 --- /dev/null +++ b/extensions/cast/README.md @@ -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 diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle new file mode 100644 index 0000000000..7d252332c9 --- /dev/null +++ b/extensions/cast/build.gradle @@ -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' diff --git a/extensions/cast/src/main/AndroidManifest.xml b/extensions/cast/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c12fc1289f --- /dev/null +++ b/extensions/cast/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java new file mode 100644 index 0000000000..ef84c04c04 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -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. + * + *

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. + * + *

Methods should be called on the application's main thread. + * + *

Known issues: + *

    + *
  • Part of the Cast API is not exposed through this interface. For instance, volume settings + * and track selection.
  • + *
  • Repeat mode is not working. See [internal: b/64137174].
  • + *
+ */ +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 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 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, 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 { + + @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 { + + @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)); + } + } + + } + +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java new file mode 100644 index 0000000000..de60437444 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java @@ -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 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 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() {} + +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java new file mode 100644 index 0000000000..06f0bec971 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -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 getAdditionalSessionProviders(Context context) { + return null; + } + +} diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a4ead9e01f..c084ec6bf8 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -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 diff --git a/library/all/src/main/AndroidManifest.xml b/library/all/src/main/AndroidManifest.xml index 1efda648e9..f31f55b40a 100644 --- a/library/all/src/main/AndroidManifest.xml +++ b/library/all/src/main/AndroidManifest.xml @@ -13,5 +13,4 @@ See the License for the specific language governing permissions and limitations under the License. --> -