mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
1cfd246cd3
commit
04d76fa8fc
@ -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'
|
||||
|
@ -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')
|
||||
|
4
demos/cast/README.md
Normal file
4
demos/cast/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Cast demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with Google Cast.
|
51
demos/cast/build.gradle
Normal file
51
demos/cast/build.gradle
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'extension-cast')
|
||||
}
|
43
demos/cast/src/main/AndroidManifest.xml
Normal file
43
demos/cast/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.castdemo"
|
||||
android:versionCode="0001"
|
||||
android:versionName="0.0.1">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTop" android:label="@string/application_name"
|
||||
android:theme="@style/Theme.AppCompat">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility methods and constants for the Cast demo application.
|
||||
*/
|
||||
/* package */ final class CastDemoUtil {
|
||||
|
||||
public static final String MIME_TYPE_DASH = "application/dash+xml";
|
||||
public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl";
|
||||
public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml";
|
||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||
|
||||
/**
|
||||
* The list of samples available in the cast demo app.
|
||||
*/
|
||||
public static final List<Sample> SAMPLES;
|
||||
|
||||
/**
|
||||
* Represents a media sample.
|
||||
*/
|
||||
public static final class Sample {
|
||||
|
||||
/**
|
||||
* The uri from which the media sample is obtained.
|
||||
*/
|
||||
public final String uri;
|
||||
/**
|
||||
* A descriptive name for the sample.
|
||||
*/
|
||||
public final String name;
|
||||
/**
|
||||
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
|
||||
*/
|
||||
public final String type;
|
||||
|
||||
/**
|
||||
* @param uri See {@link #uri}.
|
||||
* @param name See {@link #name}.
|
||||
* @param type See {@link #type}.
|
||||
*/
|
||||
public Sample(String uri, String name, String type) {
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static {
|
||||
// App samples.
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
"DASH (clear,MP4,H264)", MIME_TYPE_DASH));
|
||||
samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
||||
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
|
||||
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
|
||||
MIME_TYPE_VIDEO_MP4));
|
||||
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
|
||||
}
|
||||
|
||||
private CastDemoUtil() {}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ui.PlaybackControlView;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
|
||||
/**
|
||||
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private SimpleExoPlayerView simpleExoPlayerView;
|
||||
private PlaybackControlView castControlView;
|
||||
private PlayerManager playerManager;
|
||||
|
||||
// Activity lifecycle methods.
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
|
||||
simpleExoPlayerView.requestFocus();
|
||||
|
||||
castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view);
|
||||
|
||||
ListView sampleList = (ListView) findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleListAdapter());
|
||||
sampleList.setOnItemClickListener(new SampleClickListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
|
||||
R.id.media_route_menu_item);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if (Util.SDK_INT > 23) {
|
||||
setupPlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if ((Util.SDK_INT <= 23)) {
|
||||
setupPlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (Util.SDK_INT <= 23) {
|
||||
releasePlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (Util.SDK_INT > 23) {
|
||||
releasePlayerManager();
|
||||
}
|
||||
}
|
||||
|
||||
// Activity input.
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// If the event was not handled then see if the player view can handle it.
|
||||
return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void setupPlayerManager() {
|
||||
playerManager = new PlayerManager(simpleExoPlayerView, castControlView,
|
||||
getApplicationContext());
|
||||
}
|
||||
|
||||
private void releasePlayerManager() {
|
||||
playerManager.release();
|
||||
playerManager = null;
|
||||
}
|
||||
|
||||
// User controls.
|
||||
|
||||
private final class SampleListAdapter extends ArrayAdapter<CastDemoUtil.Sample> {
|
||||
|
||||
public SampleListAdapter() {
|
||||
super(getApplicationContext(), android.R.layout.simple_list_item_1, CastDemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
view.setBackgroundColor(Color.WHITE);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SampleClickListener implements AdapterView.OnItemClickListener {
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (parent.getSelectedItemPosition() != position) {
|
||||
CastDemoUtil.Sample currentSample = CastDemoUtil.SAMPLES.get(position);
|
||||
playerManager.setCurrentSample(currentSample, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlaybackControlView;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
|
||||
/**
|
||||
* Manages players for the ExoPlayer/Cast integration app.
|
||||
*/
|
||||
/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener {
|
||||
|
||||
private static final int PLAYBACK_REMOTE = 1;
|
||||
private static final int PLAYBACK_LOCAL = 2;
|
||||
|
||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
|
||||
|
||||
private final SimpleExoPlayerView exoPlayerView;
|
||||
private final PlaybackControlView castControlView;
|
||||
private final CastContext castContext;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
|
||||
private int playbackLocation;
|
||||
private CastDemoUtil.Sample currentSample;
|
||||
|
||||
/**
|
||||
* @param exoPlayerView The {@link SimpleExoPlayerView} for local playback.
|
||||
* @param castControlView The {@link PlaybackControlView} to control remote playback.
|
||||
* @param context A {@link Context}.
|
||||
*/
|
||||
public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView,
|
||||
Context context) {
|
||||
this.exoPlayerView = exoPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
castContext = CastContext.getSharedInstance(context);
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
exoPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
castPlayer = new CastPlayer(castContext);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the given sample at the given position.
|
||||
*
|
||||
* @param currentSample The {@link CastDemoUtil} to play.
|
||||
* @param positionMs The position at which playback should start.
|
||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||
*/
|
||||
public void setCurrentSample(CastDemoUtil.Sample currentSample, long positionMs,
|
||||
boolean playWhenReady) {
|
||||
this.currentSample = currentSample;
|
||||
if (playbackLocation == PLAYBACK_REMOTE) {
|
||||
castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs,
|
||||
playWhenReady);
|
||||
} else /* playbackLocation == PLAYBACK_LOCAL */ {
|
||||
exoPlayer.setPlayWhenReady(playWhenReady);
|
||||
exoPlayer.seekTo(positionMs);
|
||||
exoPlayer.prepare(buildMediaSource(currentSample), true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current
|
||||
* playback location.
|
||||
*
|
||||
* @param event The {@link KeyEvent}.
|
||||
* @return Whether the event was handled by the target view.
|
||||
*/
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (playbackLocation == PLAYBACK_REMOTE) {
|
||||
return castControlView.dispatchKeyEvent(event);
|
||||
} else /* playbackLocation == PLAYBACK_REMOTE */ {
|
||||
return exoPlayerView.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the manager and the players that it holds.
|
||||
*/
|
||||
public void release() {
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
exoPlayerView.setPlayer(null);
|
||||
exoPlayer.release();
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
||||
@Override
|
||||
public void onCastSessionAvailable() {
|
||||
setPlaybackLocation(PLAYBACK_REMOTE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCastSessionUnavailable() {
|
||||
setPlaybackLocation(PLAYBACK_LOCAL);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) {
|
||||
Uri uri = Uri.parse(sample.uri);
|
||||
switch (sample.type) {
|
||||
case CastDemoUtil.MIME_TYPE_SS:
|
||||
return new SsMediaSource(uri, DATA_SOURCE_FACTORY,
|
||||
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
|
||||
case CastDemoUtil.MIME_TYPE_DASH:
|
||||
return new DashMediaSource(uri, DATA_SOURCE_FACTORY,
|
||||
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
|
||||
case CastDemoUtil.MIME_TYPE_HLS:
|
||||
return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null);
|
||||
case CastDemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(),
|
||||
null, null);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + sample.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPlaybackLocation(int playbackLocation) {
|
||||
if (this.playbackLocation == playbackLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// View management.
|
||||
if (playbackLocation == PLAYBACK_LOCAL) {
|
||||
exoPlayerView.setVisibility(View.VISIBLE);
|
||||
castControlView.hide();
|
||||
} else {
|
||||
exoPlayerView.setVisibility(View.GONE);
|
||||
castControlView.show();
|
||||
}
|
||||
|
||||
long playbackPositionMs = 0;
|
||||
boolean playWhenReady = true;
|
||||
if (exoPlayer != null) {
|
||||
playbackPositionMs = exoPlayer.getCurrentPosition();
|
||||
playWhenReady = exoPlayer.getPlayWhenReady();
|
||||
} else if (this.playbackLocation == PLAYBACK_REMOTE) {
|
||||
playbackPositionMs = castPlayer.getCurrentPosition();
|
||||
playWhenReady = castPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
this.playbackLocation = playbackLocation;
|
||||
if (currentSample != null) {
|
||||
setCurrentSample(currentSample, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
41
demos/cast/src/main/res/layout/main_activity.xml
Normal file
41
demos/cast/src/main/res/layout/main_activity.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
app:repeat_toggle_modes="all|one"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12" />
|
||||
<ListView
|
||||
android:id="@+id/sample_list"
|
||||
android:choiceMode="singleChoice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12" />
|
||||
<com.google.android.exoplayer2.ui.PlaybackControlView
|
||||
android:id="@+id/cast_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:show_timeout="-1"
|
||||
android:layout_weight="2"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
25
demos/cast/src/main/res/menu/menu.xml
Normal file
25
demos/cast/src/main/res/menu/menu.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/media_route_menu_item"
|
||||
android:title="@string/media_route_menu_title"
|
||||
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
BIN
demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
BIN
demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
25
demos/cast/src/main/res/values/strings.xml
Normal file
25
demos/cast/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="application_name">ExoCast Demo</string>
|
||||
|
||||
<string name="media_route_menu_title">ExoCast</string>
|
||||
|
||||
<string name="error_unsupported_drm">DRM scheme not supported by this device.</string>
|
||||
|
||||
</resources>
|
22
demos/cast/src/main/res/values/styles.xml
Normal file
22
demos/cast/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
33
extensions/cast/README.md
Normal file
33
extensions/cast/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# ExoPlayer Cast extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The cast extension is a [Player][] implementation that controls playback on a
|
||||
Cast receiver app.
|
||||
|
||||
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-cast:rX.X.X'
|
||||
```
|
||||
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
Create a `CastPlayer` and use it to integrate Cast into your app using
|
||||
ExoPlayer's common Player interface. You can try the Cast Extension to see how a
|
||||
[PlaybackControlView][] can be used to control playback in a remote receiver app.
|
||||
|
||||
[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html
|
45
extensions/cast/build.gradle
Normal file
45
extensions/cast/build.gradle
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Cast extension'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-cast'
|
||||
releaseDescription = 'Cast extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
16
extensions/cast/src/main/AndroidManifest.xml
Normal file
16
extensions/cast/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest package="com.google.android.exoplayer2.ext.cast"/>
|
@ -0,0 +1,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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility methods for ExoPlayer/Cast integration.
|
||||
*/
|
||||
/* package */ final class CastUtils {
|
||||
|
||||
private static final Map<Integer, String> CAST_STATUS_CODE_TO_STRING;
|
||||
|
||||
/**
|
||||
* Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
|
||||
* {@link CastStatusCodes}.
|
||||
*
|
||||
* @param statusCode A Cast API status code.
|
||||
* @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
|
||||
* {@link CastStatusCodes}.
|
||||
*/
|
||||
public static String getLogString(int statusCode) {
|
||||
String description = CAST_STATUS_CODE_TO_STRING.get(statusCode);
|
||||
return description != null ? description : "Unknown.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Format} instance containing all information contained in the given
|
||||
* {@link MediaTrack} object.
|
||||
*
|
||||
* @param mediaTrack The {@link MediaTrack}.
|
||||
* @return The equivalent {@link Format}.
|
||||
*/
|
||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
|
||||
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
|
||||
}
|
||||
|
||||
static {
|
||||
HashMap<Integer, String> statusCodeToString = new HashMap<>();
|
||||
statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_FOUND,
|
||||
"A requested application could not be found.");
|
||||
statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_RUNNING,
|
||||
"A requested application is not currently running.");
|
||||
statusCodeToString.put(CastStatusCodes.AUTHENTICATION_FAILED, "Authentication failure.");
|
||||
statusCodeToString.put(CastStatusCodes.CANCELED, "An in-progress request has been "
|
||||
+ "canceled, most likely because another action has preempted it.");
|
||||
statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_CREATION_FAILED,
|
||||
"The Cast Remote Display service could not be created.");
|
||||
statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_DISCONNECTED,
|
||||
"The Cast Remote Display service was disconnected.");
|
||||
statusCodeToString.put(CastStatusCodes.FAILED, "The in-progress request failed.");
|
||||
statusCodeToString.put(CastStatusCodes.INTERNAL_ERROR, "An internal error has occurred.");
|
||||
statusCodeToString.put(CastStatusCodes.INTERRUPTED,
|
||||
"A blocking call was interrupted while waiting and did not run to completion.");
|
||||
statusCodeToString.put(CastStatusCodes.INVALID_REQUEST, "An invalid request was made.");
|
||||
statusCodeToString.put(CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL, "A message could "
|
||||
+ "not be sent because there is not enough room in the send buffer at this time.");
|
||||
statusCodeToString.put(CastStatusCodes.MESSAGE_TOO_LARGE,
|
||||
"A message could not be sent because it is too large.");
|
||||
statusCodeToString.put(CastStatusCodes.NETWORK_ERROR, "Network I/O error.");
|
||||
statusCodeToString.put(CastStatusCodes.NOT_ALLOWED,
|
||||
"The request was disallowed and could not be completed.");
|
||||
statusCodeToString.put(CastStatusCodes.REPLACED,
|
||||
"The request's progress is no longer being tracked because another request of the same type"
|
||||
+ " has been made before the first request completed.");
|
||||
statusCodeToString.put(CastStatusCodes.SUCCESS, "Success.");
|
||||
statusCodeToString.put(CastStatusCodes.TIMEOUT, "An operation has timed out.");
|
||||
statusCodeToString.put(CastStatusCodes.UNKNOWN_ERROR,
|
||||
"An unknown, unexpected error has occurred.");
|
||||
CAST_STATUS_CODE_TO_STRING = Collections.unmodifiableMap(statusCodeToString);
|
||||
}
|
||||
|
||||
private CastUtils() {}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
||||
import com.google.android.gms.cast.framework.CastOptions;
|
||||
import com.google.android.gms.cast.framework.OptionsProvider;
|
||||
import com.google.android.gms.cast.framework.SessionProvider;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A convenience {@link OptionsProvider} to target the default cast receiver app.
|
||||
*/
|
||||
public final class DefaultCastOptionsProvider implements OptionsProvider {
|
||||
|
||||
@Override
|
||||
public CastOptions getCastOptions(Context context) {
|
||||
return new CastOptions.Builder()
|
||||
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
|
||||
.setStopReceiverApplicationWhenEndingSession(true).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -13,5 +13,4 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2"/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user