mirror of
https://github.com/samsonjs/media.git
synced 2026-04-04 11:05:47 +00:00
commit
eb9c67fbc5
659 changed files with 28761 additions and 12888 deletions
127
RELEASENOTES.md
127
RELEASENOTES.md
|
|
@ -1,5 +1,130 @@
|
|||
# Release notes #
|
||||
|
||||
### 2.7.0 ###
|
||||
|
||||
* Player interface:
|
||||
* Add optional parameter to `stop` to reset the player when stopping.
|
||||
* Add a reason to `EventListener.onTimelineChanged` to distinguish between
|
||||
initial preparation, reset and dynamic updates.
|
||||
* Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons
|
||||
reported in `Eventlistener.onPositionDiscontinuity` to distinguish
|
||||
transitions to and from ads within one period from transitions between
|
||||
periods.
|
||||
* Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow
|
||||
more customization of the message. Now supports setting a message delivery
|
||||
playback position and/or a delivery handler
|
||||
([#2189](https://github.com/google/ExoPlayer/issues/2189)).
|
||||
* Add `Player.VideoComponent`, `Player.TextComponent` and
|
||||
`Player.MetadataComponent` interfaces that define optional video, text and
|
||||
metadata output functionality. New `getVideoComponent`, `getTextComponent`
|
||||
and `getMetadataComponent` methods provide access to this functionality.
|
||||
* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are
|
||||
performed. The `SeekParameters` class contains defaults for exact seeking and
|
||||
seeking to the closest sync points before, either side or after specified seek
|
||||
positions. `SeekParameters` are not currently supported when playing HLS
|
||||
streams.
|
||||
* DefaultTrackSelector:
|
||||
* Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
|
||||
* Support disabling of individual text track selection flags.
|
||||
* Buffering:
|
||||
* Allow a back-buffer of media to be retained behind the current playback
|
||||
position, for fast backward seeking. The back-buffer can be configured by
|
||||
custom `LoadControl` implementations.
|
||||
* Add ability for `SequenceableLoader` to re-evaluate its buffer and discard
|
||||
buffered media so that it can be re-buffered in a different quality.
|
||||
* Allow more flexible loading strategy when playing media containing multiple
|
||||
sub-streams, by allowing injection of custom `CompositeSequenceableLoader`
|
||||
factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`,
|
||||
`SsMediaSource.Factory`, and `MergingMediaSource`.
|
||||
* Play out existing buffer before retrying for progressive live streams
|
||||
([#1606](https://github.com/google/ExoPlayer/issues/1606)).
|
||||
* UI components:
|
||||
* Generalized player and control views to allow them to bind with any
|
||||
`Player`, and renamed them to `PlayerView` and `PlayerControlView`
|
||||
respectively.
|
||||
* Made `PlayerView` automatically apply video rotation when configured to use
|
||||
`TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)).
|
||||
* Made `PlayerView` play button behave correctly when the player is ended
|
||||
([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a
|
||||
`PlaybackPreparer` when the player is idle.
|
||||
* DRM: Optimistically attempt playback of DRM protected content that does not
|
||||
declare scheme specific init data in the manifest. If playback of clear
|
||||
samples without keys is allowed, delay DRM session error propagation until
|
||||
keys are actually needed
|
||||
([#3630](https://github.com/google/ExoPlayer/issues/3630)).
|
||||
* DASH:
|
||||
* Support in-band Emsg events targeting the player with scheme id
|
||||
"urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3".
|
||||
* Support EventStream elements in DASH manifests.
|
||||
* HLS:
|
||||
* Add opt-in support for chunkless preparation in HLS. This allows an
|
||||
HLS source to finish preparation without downloading any chunks, which can
|
||||
significantly reduce initial buffering time
|
||||
([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details
|
||||
can be found
|
||||
[here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6).
|
||||
* Fail if unable to sync with the Transport Stream, rather than entering
|
||||
stuck in an indefinite buffering state.
|
||||
* Fix mime type propagation
|
||||
([#3653](https://github.com/google/ExoPlayer/issues/3653)).
|
||||
* Fix ID3 context reuse across segment format changes
|
||||
([#3622](https://github.com/google/ExoPlayer/issues/3622)).
|
||||
* Use long for media sequence numbers
|
||||
([#3747](https://github.com/google/ExoPlayer/issues/3747))
|
||||
* Add initial support for the EXT-X-GAP tag.
|
||||
* Audio:
|
||||
* Support TrueHD passthrough for rechunked samples in Matroska files
|
||||
([#2147](https://github.com/google/ExoPlayer/issues/2147)).
|
||||
* Support resampling 24-bit and 32-bit integer to 32-bit float for high
|
||||
resolution output in `DefaultAudioSink`
|
||||
([#3635](https://github.com/google/ExoPlayer/pull/3635)).
|
||||
* Captions:
|
||||
* Basic support for PGS subtitles
|
||||
([#3008](https://github.com/google/ExoPlayer/issues/3008)).
|
||||
* Fix handling of CEA-608 captions where multiple buffers have the same
|
||||
presentation timestamp
|
||||
([#3782](https://github.com/google/ExoPlayer/issues/3782)).
|
||||
* Caching:
|
||||
* Fix cache corruption issue
|
||||
([#3762](https://github.com/google/ExoPlayer/issues/3762)).
|
||||
* Implement periodic check in `CacheDataSource` to see whether it's possible
|
||||
to switch to reading/writing the cache having initially bypassed it.
|
||||
* IMA extension:
|
||||
* Fix the player getting stuck when an ad group fails to load
|
||||
([#3584](https://github.com/google/ExoPlayer/issues/3584)).
|
||||
* Work around loadAd not being called beore the LOADED AdEvent arrives
|
||||
([#3552](https://github.com/google/ExoPlayer/issues/3552)).
|
||||
* Handle asset mismatch errors
|
||||
([#3801](https://github.com/google/ExoPlayer/issues/3801)).
|
||||
* Add support for playing non-Extractor content MediaSources in
|
||||
the IMA demo app
|
||||
([#3676](https://github.com/google/ExoPlayer/issues/3676)).
|
||||
* Fix handling of ad tags where ad groups are out of order
|
||||
([#3716](https://github.com/google/ExoPlayer/issues/3716)).
|
||||
* Fix handling of ad tags with only preroll/postroll ad groups
|
||||
([#3715](https://github.com/google/ExoPlayer/issues/3715)).
|
||||
* Propagate ad media preparation errors to IMA so that the ads can be
|
||||
skipped.
|
||||
* Handle exceptions in IMA callbacks so that can be logged less verbosely.
|
||||
* New Cast extension. Simplifies toggling between local and Cast playbacks.
|
||||
* `EventLogger` moved from the demo app into the core library.
|
||||
* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C,
|
||||
Lenovo K4 Note and Sony Xperia E5.
|
||||
([#3724](https://github.com/google/ExoPlayer/issues/3724),
|
||||
[#3835](https://github.com/google/ExoPlayer/issues/3835)).
|
||||
* Fix potential NPE when removing media sources from a
|
||||
DynamicConcatenatingMediaSource
|
||||
([#3796](https://github.com/google/ExoPlayer/issues/3796)).
|
||||
* Check `sys.display-size` on Philips ATVs
|
||||
([#3807](https://github.com/google/ExoPlayer/issues/3807)).
|
||||
* Release `Extractor`s on the loading thread to avoid potentially leaking
|
||||
resources when the playback thread has quit by the time the loading task has
|
||||
completed.
|
||||
* ID3: Better handle malformed ID3 data
|
||||
([#3792](https://github.com/google/ExoPlayer/issues/3792).
|
||||
* Support 14-bit mode and little endianness in DTS PES packets
|
||||
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
|
||||
|
||||
### 2.6.1 ###
|
||||
|
||||
* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,
|
||||
|
|
@ -41,6 +166,8 @@
|
|||
([#3188](https://github.com/google/ExoPlayer/issues/3188)).
|
||||
* CEA-608: Fix handling of row count changes in roll-up mode
|
||||
([#3513](https://github.com/google/ExoPlayer/issues/3513)).
|
||||
* Prevent period transitions when seeking to the end of a period when paused
|
||||
([#2439](https://github.com/google/ExoPlayer/issues/2439)).
|
||||
|
||||
### 2.6.0 ###
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ project.ext {
|
|||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
junitVersion = '4.12'
|
||||
truthVersion = '0.35'
|
||||
robolectricVersion = '3.4.2'
|
||||
releaseVersion = '2.6.1'
|
||||
truthVersion = '0.39'
|
||||
robolectricVersion = '3.7.1'
|
||||
releaseVersion = '2.7.0'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
52
demos/cast/build.gradle
Normal file
52
demos/cast/build.gradle
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// 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')
|
||||
compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
||||
}
|
||||
42
demos/cast/src/main/AndroidManifest.xml
Normal file
42
demos/cast/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?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="2700"
|
||||
android:versionName="2.7.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
|
||||
|
||||
<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"/>
|
||||
</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 DemoUtil {
|
||||
|
||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
||||
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
|
||||
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 mimeType;
|
||||
|
||||
/**
|
||||
* @param uri See {@link #uri}.
|
||||
* @param name See {@link #name}.
|
||||
* @param mimeType See {@link #mimeType}.
|
||||
*/
|
||||
public Sample(String uri, String name, String mimeType) {
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
@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 DemoUtil() {}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* 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.os.Bundle;
|
||||
import android.support.v4.graphics.ColorUtils;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
|
||||
/**
|
||||
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||
PlayerManager.QueuePositionListener {
|
||||
|
||||
private PlayerView localPlayerView;
|
||||
private PlayerControlView castControlView;
|
||||
private PlayerManager playerManager;
|
||||
private RecyclerView mediaQueueList;
|
||||
private MediaQueueListAdapter mediaQueueListAdapter;
|
||||
private CastContext castContext;
|
||||
|
||||
// Activity lifecycle methods.
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
||||
castContext = CastContext.getSharedInstance(this);
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
localPlayerView = findViewById(R.id.local_player_view);
|
||||
localPlayerView.requestFocus();
|
||||
|
||||
castControlView = findViewById(R.id.cast_control_view);
|
||||
|
||||
mediaQueueList = findViewById(R.id.sample_list);
|
||||
ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());
|
||||
helper.attachToRecyclerView(mediaQueueList);
|
||||
mediaQueueList.setLayoutManager(new LinearLayoutManager(this));
|
||||
mediaQueueList.setHasFixedSize(true);
|
||||
mediaQueueListAdapter = new MediaQueueListAdapter();
|
||||
|
||||
findViewById(R.id.add_sample_button).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
playerManager =
|
||||
PlayerManager.createPlayerManager(
|
||||
/* queuePositionListener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
castContext);
|
||||
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||
mediaQueueList.setAdapter(null);
|
||||
playerManager.release();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title)
|
||||
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create()
|
||||
.show();
|
||||
}
|
||||
|
||||
// PlayerManager.QueuePositionListener implementation.
|
||||
|
||||
@Override
|
||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||
if (previousIndex != C.INDEX_UNSET) {
|
||||
mediaQueueListAdapter.notifyItemChanged(previousIndex);
|
||||
}
|
||||
if (newIndex != C.INDEX_UNSET) {
|
||||
mediaQueueListAdapter.notifyItemChanged(newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private View buildSampleListView() {
|
||||
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
|
||||
ListView sampleList = dialogList.findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleListAdapter(this));
|
||||
sampleList.setOnItemClickListener(
|
||||
new OnItemClickListener() {
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||
}
|
||||
});
|
||||
return dialogList;
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
||||
|
||||
public final TextView textView;
|
||||
|
||||
public QueueItemViewHolder(TextView textView) {
|
||||
super(textView);
|
||||
this.textView = textView;
|
||||
textView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playerManager.selectQueueItem(getAdapterPosition());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||
|
||||
@Override
|
||||
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
TextView v = (TextView) LayoutInflater.from(parent.getContext())
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
return new QueueItemViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||
TextView view = holder.textView;
|
||||
view.setText(playerManager.getItem(position).name);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return playerManager.getMediaQueueSize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private int draggingFromPosition;
|
||||
private int draggingToPosition;
|
||||
|
||||
public RecyclerViewCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
|
||||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int fromPosition = origin.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
if (draggingFromPosition == C.INDEX_UNSET) {
|
||||
// A drag has started, but changes to the media queue will be reflected in clearView().
|
||||
draggingFromPosition = fromPosition;
|
||||
}
|
||||
draggingToPosition = toPosition;
|
||||
mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
if (playerManager.removeItem(position)) {
|
||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||
// A drag has ended. We reflect the media queue change in the player.
|
||||
if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) {
|
||||
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
||||
// invalidated.
|
||||
mediaQueueListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SampleListAdapter extends ArrayAdapter<Sample> {
|
||||
|
||||
public SampleListAdapter(Context context) {
|
||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
/*
|
||||
* 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.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DefaultEventListener;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||
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.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Manages players and an internal media queue for the ExoPlayer/Cast demo app.
|
||||
*/
|
||||
/* package */ final class PlayerManager extends DefaultEventListener
|
||||
implements CastPlayer.SessionAvailabilityListener {
|
||||
|
||||
/**
|
||||
* Listener for changes in the media queue playback position.
|
||||
*/
|
||||
public interface QueuePositionListener {
|
||||
|
||||
/**
|
||||
* Called when the currently played item of the media queue changes.
|
||||
*/
|
||||
void onQueuePositionChanged(int previousIndex, int newIndex);
|
||||
|
||||
}
|
||||
|
||||
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 PlayerView localPlayerView;
|
||||
private final PlayerControlView castControlView;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<DemoUtil.Sample> mediaQueue;
|
||||
private final QueuePositionListener queuePositionListener;
|
||||
|
||||
private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
|
||||
private boolean castMediaQueueCreationPending;
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
|
||||
/**
|
||||
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
|
||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||
* @param context A {@link Context}.
|
||||
* @param castContext The {@link CastContext}.
|
||||
*/
|
||||
public static PlayerManager createPlayerManager(
|
||||
QueuePositionListener queuePositionListener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
PlayerManager playerManager =
|
||||
new PlayerManager(
|
||||
queuePositionListener, localPlayerView, castControlView, context, castContext);
|
||||
playerManager.init();
|
||||
return playerManager;
|
||||
}
|
||||
|
||||
private PlayerManager(
|
||||
QueuePositionListener queuePositionListener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
this.queuePositionListener = queuePositionListener;
|
||||
this.localPlayerView = localPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
castPlayer = new CastPlayer(castContext);
|
||||
castPlayer.addListener(this);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
}
|
||||
|
||||
// Queue manipulation methods.
|
||||
|
||||
/**
|
||||
* Plays a specified queue item in the current player.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
*/
|
||||
public void selectQueueItem(int itemIndex) {
|
||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the currently played item.
|
||||
*/
|
||||
public int getCurrentItemIndex() {
|
||||
return currentItemIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends {@code sample} to the media queue.
|
||||
*
|
||||
* @param sample The {@link Sample} to append.
|
||||
*/
|
||||
public void addItem(Sample sample) {
|
||||
mediaQueue.add(sample);
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
||||
} else {
|
||||
castPlayer.addItems(buildMediaQueueItem(sample));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the media queue.
|
||||
*/
|
||||
public int getMediaQueueSize() {
|
||||
return mediaQueue.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item at the given index in the media queue.
|
||||
*
|
||||
* @param position The index of the item.
|
||||
* @return The item at the given index in the media queue.
|
||||
*/
|
||||
public Sample getItem(int position) {
|
||||
return mediaQueue.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item at the given index from the media queue.
|
||||
*
|
||||
* @param itemIndex The index of the item to remove.
|
||||
* @return Whether the removal was successful.
|
||||
*/
|
||||
public boolean removeItem(int itemIndex) {
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
} else {
|
||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||
return false;
|
||||
}
|
||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
||||
}
|
||||
}
|
||||
mediaQueue.remove(itemIndex);
|
||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||
} else if (itemIndex < currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item within the queue.
|
||||
*
|
||||
* @param fromIndex The index of the item to move.
|
||||
* @param toIndex The target index of the item in the queue.
|
||||
* @return Whether the item move was successful.
|
||||
*/
|
||||
public boolean moveItem(int fromIndex, int toIndex) {
|
||||
// Player update.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
} else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
int periodCount = castTimeline.getPeriodCount();
|
||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||
return false;
|
||||
}
|
||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
||||
castPlayer.moveItem(elementId, toIndex);
|
||||
}
|
||||
|
||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
||||
|
||||
// Index update.
|
||||
if (fromIndex == currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(toIndex);
|
||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Miscellaneous methods.
|
||||
|
||||
/**
|
||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||
*
|
||||
* @param event The {@link KeyEvent}.
|
||||
* @return Whether the event was handled by the target view.
|
||||
*/
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (currentPlayer == exoPlayer) {
|
||||
return localPlayerView.dispatchKeyEvent(event);
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
return castControlView.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the manager and the players that it holds.
|
||||
*/
|
||||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
localPlayerView.setPlayer(null);
|
||||
exoPlayer.release();
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
||||
@Override
|
||||
public void onCastSessionAvailable() {
|
||||
setCurrentPlayer(castPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCastSessionUnavailable() {
|
||||
setCurrentPlayer(exoPlayer);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void init() {
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
private void updateCurrentItemIndex() {
|
||||
int playbackState = currentPlayer.getPlaybackState();
|
||||
maybeSetCurrentItemAndNotify(
|
||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
private void setCurrentPlayer(Player currentPlayer) {
|
||||
if (this.currentPlayer == currentPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// View management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
localPlayerView.setVisibility(View.VISIBLE);
|
||||
castControlView.hide();
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
localPlayerView.setVisibility(View.GONE);
|
||||
castControlView.show();
|
||||
}
|
||||
|
||||
// Player state management.
|
||||
long playbackPositionMs = C.TIME_UNSET;
|
||||
int windowIndex = C.INDEX_UNSET;
|
||||
boolean playWhenReady = false;
|
||||
if (this.currentPlayer != null) {
|
||||
int playbackState = this.currentPlayer.getPlaybackState();
|
||||
if (playbackState != Player.STATE_ENDED) {
|
||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
||||
if (windowIndex != currentItemIndex) {
|
||||
playbackPositionMs = C.TIME_UNSET;
|
||||
windowIndex = currentItemIndex;
|
||||
}
|
||||
}
|
||||
this.currentPlayer.stop(true);
|
||||
} else {
|
||||
// This is the initial setup. No need to save any state.
|
||||
}
|
||||
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource();
|
||||
for (int i = 0; i < mediaQueue.size(); i++) {
|
||||
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
|
||||
}
|
||||
exoPlayer.prepare(dynamicConcatenatingMediaSource);
|
||||
}
|
||||
|
||||
// Playback transition.
|
||||
if (windowIndex != C.INDEX_UNSET) {
|
||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the item at the given position.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
* @param positionMs The position at which playback should start.
|
||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||
*/
|
||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (castMediaQueueCreationPending) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castMediaQueueCreationPending = false;
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||
if (this.currentItemIndex != currentItemIndex) {
|
||||
int oldIndex = this.currentItemIndex;
|
||||
this.currentItemIndex = currentItemIndex;
|
||||
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaSource buildMediaSource(DemoUtil.Sample sample) {
|
||||
Uri uri = Uri.parse(sample.uri);
|
||||
switch (sample.mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
||||
.createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
||||
.createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + sample.mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) {
|
||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
|
||||
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
|
||||
.setMetadata(movieMetadata).build();
|
||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?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.
|
||||
-->
|
||||
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
||||
</vector>
|
||||
52
demos/cast/src/main/res/layout/main_activity.xml
Normal file
52
demos/cast/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?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.PlayerView android:id="@+id/local_player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12"
|
||||
app:repeat_toggle_modes="all|one"/>
|
||||
<RelativeLayout android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12">
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
|
||||
android:choiceMode="singleChoice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"/>
|
||||
<ImageButton android:id="@+id/add_sample_button"
|
||||
android:background="@drawable/ic_add_circle_white_24dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:padding="30dp"/>
|
||||
</RelativeLayout>
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="2"
|
||||
android:visibility="gone"
|
||||
app:repeat_toggle_modes="all|one"
|
||||
app:show_timeout="-1"/>
|
||||
</LinearLayout>
|
||||
25
demos/cast/src/main/res/layout/sample_list.xml
Normal file
25
demos/cast/src/main/res/layout/sample_list.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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView android:id="@+id/sample_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:fadeScrollbars="false"/>
|
||||
|
||||
</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 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">Exo Cast Demo</string>
|
||||
|
||||
<string name="media_route_menu_title">Cast</string>
|
||||
|
||||
<string name="sample_list_dialog_title">Add samples</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -45,5 +45,6 @@ dependencies {
|
|||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'extension-ima')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo"
|
||||
android:versionCode="2601"
|
||||
android:versionName="2.6.1">
|
||||
android:versionCode="2700"
|
||||
android:versionName="2.7.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer2.imademo;
|
|||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
||||
/**
|
||||
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
|
||||
|
|
@ -26,7 +26,7 @@ import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
|||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private SimpleExoPlayerView playerView;
|
||||
private PlayerView playerView;
|
||||
private PlayerManager player;
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -32,11 +32,13 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
|||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
|
|
@ -66,7 +68,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
new DefaultBandwidthMeter());
|
||||
}
|
||||
|
||||
public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
|
||||
public void init(Context context, PlayerView playerView) {
|
||||
// Create a default track selector.
|
||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
|
|
@ -77,17 +79,12 @@ import com.google.android.exoplayer2.util.Util;
|
|||
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||
|
||||
// Bind the player to the view.
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
|
||||
// Produces DataSource instances through which media data is loaded.
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
|
||||
Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
playerView.setPlayer(player);
|
||||
|
||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource =
|
||||
new ExtractorMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(Uri.parse(contentUrl));
|
||||
buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null);
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds =
|
||||
|
|
@ -95,7 +92,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
contentMediaSource,
|
||||
/* adMediaSourceFactory= */ this,
|
||||
adsLoader,
|
||||
simpleExoPlayerView.getOverlayFrameLayout(),
|
||||
playerView.getOverlayFrameLayout(),
|
||||
/* eventHandler= */ null,
|
||||
/* eventListener= */ null);
|
||||
|
||||
|
|
@ -126,6 +123,19 @@ import com.google.android.exoplayer2.util.Util;
|
|||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
return buildMediaSource(uri, handler, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
// IMA does not support Smooth Streaming ads.
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private MediaSource buildMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
|
|
@ -133,20 +143,19 @@ import com.google.android.exoplayer2.util.Util;
|
|||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
manifestDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_SS:
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ android {
|
|||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
proguardFiles = [
|
||||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
|
|||
7
demos/main/proguard-rules.txt
Normal file
7
demos/main/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Proguard rules specific to the main demo app.
|
||||
|
||||
# Constructor accessed via reflection in PlayerActivity
|
||||
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
|
||||
<init>(android.content.Context, android.net.Uri);
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2601"
|
||||
android:versionName="2.6.1">
|
||||
android:versionCode="2700"
|
||||
android:versionName="2.7.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
|
|
|||
|
|
@ -540,7 +540,7 @@
|
|||
{
|
||||
"name": "VMAP pre-, mid- and post-rolls, single ads",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
|
||||
},
|
||||
{
|
||||
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
|
||||
|
|
@ -566,6 +566,16 @@
|
|||
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
|
||||
},
|
||||
{
|
||||
"name": "VMAP empty midroll",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll"
|
||||
},
|
||||
{
|
||||
"name": "VMAP full, empty, full midrolls",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ package com.google.android.exoplayer2.demo;
|
|||
|
||||
import android.app.Application;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
|
|
@ -36,13 +36,15 @@ public class DemoApplication extends Application {
|
|||
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
}
|
||||
|
||||
public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
|
||||
return new DefaultDataSourceFactory(this, bandwidthMeter,
|
||||
buildHttpDataSourceFactory(bandwidthMeter));
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
|
||||
return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
|
||||
}
|
||||
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
|
||||
return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
|
||||
/** Returns a {@link HttpDataSource.Factory}. */
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory(
|
||||
TransferListener<? super DataSource> listener) {
|
||||
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
||||
}
|
||||
|
||||
public boolean useExtensionRenderers() {
|
||||
|
|
|
|||
|
|
@ -16,44 +16,15 @@
|
|||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Utility methods for demo application.
|
||||
*/
|
||||
/* package */ final class DemoUtil {
|
||||
|
||||
/**
|
||||
* Derives a DRM {@link UUID} from {@code drmScheme}.
|
||||
*
|
||||
* @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or
|
||||
* {@code "clearkey"}.
|
||||
* @return The derived {@link UUID}.
|
||||
* @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}.
|
||||
*/
|
||||
public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException {
|
||||
switch (Util.toLowerInvariant(drmScheme)) {
|
||||
case "widevine":
|
||||
return C.WIDEVINE_UUID;
|
||||
case "playready":
|
||||
return C.PLAYREADY_UUID;
|
||||
case "clearkey":
|
||||
return C.CLEARKEY_UUID;
|
||||
default:
|
||||
try {
|
||||
return UUID.fromString(drmScheme);
|
||||
} catch (RuntimeException e) {
|
||||
throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a track name for display.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
|
|
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.C.ContentType;
|
|||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
|
|
@ -68,22 +68,22 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT
|
|||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlaybackControlView;
|
||||
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.EventLogger;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An activity that plays media using {@link SimpleExoPlayer}.
|
||||
*/
|
||||
public class PlayerActivity extends Activity implements OnClickListener,
|
||||
PlaybackControlView.VisibilityListener {
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
public class PlayerActivity extends Activity
|
||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL = "drm_license_url";
|
||||
|
|
@ -112,10 +112,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
|
||||
private Handler mainHandler;
|
||||
private EventLogger eventLogger;
|
||||
private SimpleExoPlayerView simpleExoPlayerView;
|
||||
private PlayerView playerView;
|
||||
private LinearLayout debugRootView;
|
||||
private TextView debugTextView;
|
||||
private Button retryButton;
|
||||
|
||||
private DataSource.Factory mediaDataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
|
|
@ -153,12 +152,10 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
rootView.setOnClickListener(this);
|
||||
debugRootView = findViewById(R.id.controls_root);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
retryButton = findViewById(R.id.retry_button);
|
||||
retryButton.setOnClickListener(this);
|
||||
|
||||
simpleExoPlayerView = findViewById(R.id.player_view);
|
||||
simpleExoPlayerView.setControllerVisibilityListener(this);
|
||||
simpleExoPlayerView.requestFocus();
|
||||
playerView = findViewById(R.id.player_view);
|
||||
playerView.setControllerVisibilityListener(this);
|
||||
playerView.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -223,16 +220,14 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// See whether the player view wants to handle media or DPAD keys events.
|
||||
return simpleExoPlayerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
|
||||
return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// OnClickListener methods
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (view == retryButton) {
|
||||
initializePlayer();
|
||||
} else if (view.getParent() == debugRootView) {
|
||||
if (view.getParent() == debugRootView) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
trackSelectionHelper.showSelectionDialog(
|
||||
|
|
@ -241,6 +236,13 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
}
|
||||
}
|
||||
|
||||
// PlaybackControlView.PlaybackPreparer implementation
|
||||
|
||||
@Override
|
||||
public void preparePlayback() {
|
||||
initializePlayer();
|
||||
}
|
||||
|
||||
// PlaybackControlView.VisibilityListener implementation
|
||||
|
||||
@Override
|
||||
|
|
@ -273,9 +275,14 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
try {
|
||||
String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
|
||||
: DRM_SCHEME_UUID_EXTRA;
|
||||
UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
|
||||
drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
|
||||
keyRequestPropertiesArray, multiSession);
|
||||
UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
|
||||
if (drmSchemeUuid == null) {
|
||||
errorStringId = R.string.error_drm_unsupported_scheme;
|
||||
} else {
|
||||
drmSessionManager =
|
||||
buildDrmSessionManagerV18(
|
||||
drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);
|
||||
}
|
||||
} catch (UnsupportedDrmException e) {
|
||||
errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
|
||||
|
|
@ -302,9 +309,10 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
player.addMetadataOutput(eventLogger);
|
||||
player.addAudioDebugListener(eventLogger);
|
||||
player.addVideoDebugListener(eventLogger);
|
||||
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
|
||||
playerView.setPlayer(player);
|
||||
playerView.setPlaybackPreparer(this);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
}
|
||||
|
|
@ -345,9 +353,10 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
try {
|
||||
mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
|
||||
} catch (Exception e) {
|
||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
|
||||
if (adsMediaSource != null) {
|
||||
mediaSource = adsMediaSource;
|
||||
} else {
|
||||
showToast(R.string.ima_not_loaded);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -455,39 +464,47 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ads media source, reusing the ads loader if one exists.
|
||||
*
|
||||
* @throws Exception Thrown if it was not possible to create an ads media source, for example, due
|
||||
* to a missing dependency.
|
||||
*/
|
||||
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception {
|
||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||
private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
||||
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
if (adsLoader == null) {
|
||||
adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class)
|
||||
.newInstance(this, adTagUri);
|
||||
adUiViewGroup = new FrameLayout(this);
|
||||
// The demo app has a non-null overlay frame layout.
|
||||
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
||||
}
|
||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||
new AdsMediaSource.MediaSourceFactory() {
|
||||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
return PlayerActivity.this.buildMediaSource(
|
||||
uri, /* overrideExtension= */ null, handler, listener);
|
||||
}
|
||||
try {
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
if (adsLoader == null) {
|
||||
// Full class names used so the LINT.IfChange rule triggers should any of the classes move.
|
||||
// LINT.IfChange
|
||||
Constructor<? extends AdsLoader> loaderConstructor =
|
||||
loaderClass
|
||||
.asSubclass(AdsLoader.class)
|
||||
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
||||
adUiViewGroup = new FrameLayout(this);
|
||||
// The demo app has a non-null overlay frame layout.
|
||||
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
||||
}
|
||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||
new AdsMediaSource.MediaSourceFactory() {
|
||||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
return PlayerActivity.this.buildMediaSource(
|
||||
uri, /* overrideExtension= */ null, handler, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
};
|
||||
return new AdsMediaSource(
|
||||
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
};
|
||||
return new AdsMediaSource(
|
||||
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// IMA extension not loaded.
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseAdsLoader() {
|
||||
|
|
@ -495,7 +512,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
adsLoader.release();
|
||||
adsLoader = null;
|
||||
loadedAdTagUri = null;
|
||||
simpleExoPlayerView.getOverlayFrameLayout().removeAllViews();
|
||||
playerView.getOverlayFrameLayout().removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -503,10 +520,6 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
|||
|
||||
private void updateButtonVisibilities() {
|
||||
debugRootView.removeAllViews();
|
||||
|
||||
retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE);
|
||||
debugRootView.addView(retryButton);
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
|
|
@ -202,11 +201,9 @@ public class SampleChooserActivity extends Activity {
|
|||
break;
|
||||
case "drm_scheme":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
||||
try {
|
||||
drmUuid = DemoUtil.getDrmUuid(reader.nextString());
|
||||
} catch (UnsupportedDrmException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
String drmScheme = reader.nextString();
|
||||
drmUuid = Util.getDrmUuid(drmScheme);
|
||||
Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme);
|
||||
break;
|
||||
case "drm_license_url":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
|
|
|
|||
7
demos/main/src/main/proguard-rules.txt
Normal file
7
demos/main/src/main/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Proguard rules specific to the main demo app.
|
||||
|
||||
# Constructor accessed via reflection in PlayerActivity
|
||||
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
|
||||
<init>(android.content.Context, android.net.Uri);
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
|
|
@ -42,15 +42,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button android:id="@+id/retry_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/retry"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@
|
|||
|
||||
<string name="text">Text</string>
|
||||
|
||||
<string name="retry">Retry</string>
|
||||
|
||||
<string name="selection_disabled">Disabled</string>
|
||||
|
||||
<string name="selection_default">Default</string>
|
||||
|
|
|
|||
30
extensions/cast/README.md
Normal file
30
extensions/cast/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 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.
|
||||
56
extensions/cast/build.gradle
Normal file
56
extensions/cast/build.gradle
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// 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 {
|
||||
// This dependency is necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
||||
// is included via:
|
||||
// com.google.android.gms:play-services-cast-framework:11.4.2
|
||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
compile 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
compile 'com.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')
|
||||
testCompile project(modulePrefix + 'testutils')
|
||||
testCompile 'junit:junit:' + junitVersion
|
||||
testCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
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,882 @@
|
|||
/*
|
||||
* 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.NonNull;
|
||||
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.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.Assertions;
|
||||
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.MediaQueueItem;
|
||||
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.PendingResult;
|
||||
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>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
||||
* Cast context passed to {@link #CastPlayer}. To keep track of the session,
|
||||
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||
* implemented and attached to the player.</p>
|
||||
*
|
||||
* <p> If no session is available, the player state will remain unchanged and calls to methods that
|
||||
* alter it will be ignored. Querying the player state is possible even when no session is
|
||||
* available, in which case, the last observed receiver app state is reported.</p>
|
||||
*
|
||||
* <p>Methods should be called on the application's main thread.</p>
|
||||
*/
|
||||
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 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;
|
||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||
private final CastTimelineTracker timelineTracker;
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
|
||||
private RemoteMediaClient remoteMediaClient;
|
||||
|
||||
// Result callbacks.
|
||||
private final StatusListener statusListener;
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
|
||||
// Listeners.
|
||||
private final CopyOnWriteArraySet<EventListener> listeners;
|
||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
private CastTimeline currentTimeline;
|
||||
private TrackGroupArray currentTrackGroups;
|
||||
private TrackSelectionArray currentTrackSelection;
|
||||
private int playbackState;
|
||||
private int repeatMode;
|
||||
private int currentWindowIndex;
|
||||
private boolean playWhenReady;
|
||||
private long lastReportedPositionMs;
|
||||
private int pendingSeekCount;
|
||||
private int pendingSeekWindowIndex;
|
||||
private long pendingSeekPositionMs;
|
||||
private boolean waitingForInitialTimeline;
|
||||
|
||||
/**
|
||||
* @param castContext The context from which the cast session is obtained.
|
||||
*/
|
||||
public CastPlayer(CastContext castContext) {
|
||||
this.castContext = castContext;
|
||||
timelineTracker = new CastTimelineTracker();
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
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;
|
||||
|
||||
playbackState = STATE_IDLE;
|
||||
repeatMode = REPEAT_MODE_OFF;
|
||||
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
currentTrackGroups = TrackGroupArray.EMPTY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
updateInternalState();
|
||||
}
|
||||
|
||||
// Media Queue manipulation methods.
|
||||
|
||||
/**
|
||||
* Loads a single item media queue. If no session is available, does nothing.
|
||||
*
|
||||
* @param item The item to load.
|
||||
* @param positionMs The position at which the playback should start in milliseconds relative to
|
||||
* the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback
|
||||
* starts at position 0.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
||||
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a media queue. If no session is available, does nothing.
|
||||
*
|
||||
* @param items The items to load.
|
||||
* @param startIndex The index of the item at which playback should start.
|
||||
* @param positionMs The position at which the playback should start in milliseconds relative to
|
||||
* the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback
|
||||
* starts at position 0.
|
||||
* @param repeatMode The repeat mode for the created media queue.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
|
||||
long positionMs, @RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||
waitingForInitialTimeline = true;
|
||||
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
|
||||
positionMs, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a sequence of items to the media queue. If no media queue exists, does nothing.
|
||||
*
|
||||
* @param items The items to append.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
||||
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a sequence of items into the media queue. If no media queue or period with id {@code
|
||||
* periodId} exist, does nothing.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* that will follow immediately after the inserted items.
|
||||
* @param items The items to insert.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
||||
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
|
||||
return remoteMediaClient.queueInsertItems(items, periodId, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the media queue. If no media queue or period with id {@code periodId}
|
||||
* exist, does nothing.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* to remove.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return remoteMediaClient.queueRemoveItem(periodId, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an existing item within the media queue. If no media queue or period with id {@code
|
||||
* periodId} exist, does nothing.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* to move.
|
||||
* @param newIndex The target index of the item in the media queue. Must be in the range 0 <=
|
||||
* index < {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item that corresponds to the period with the given id, or null if no media queue or
|
||||
* period with id {@code periodId} exist.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* to get.
|
||||
* @return The item that corresponds to the period with the given id, or null if no media queue or
|
||||
* period with id {@code periodId} exist.
|
||||
*/
|
||||
public MediaQueueItem getItem(int periodId) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
|
||||
? mediaStatus.getItemById(periodId) : null;
|
||||
}
|
||||
|
||||
// CastSession methods.
|
||||
|
||||
/**
|
||||
* Returns whether a cast session is available.
|
||||
*/
|
||||
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 VideoComponent getVideoComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextComponent getTextComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
if (remoteMediaClient == null) {
|
||||
return;
|
||||
}
|
||||
if (playWhenReady) {
|
||||
remoteMediaClient.play();
|
||||
} else {
|
||||
remoteMediaClient.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return playWhenReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToDefaultPosition() {
|
||||
seekTo(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToDefaultPosition(int windowIndex) {
|
||||
seekTo(windowIndex, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(long positionMs) {
|
||||
seekTo(getCurrentWindowIndex(), positionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int windowIndex, long positionMs) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
// We assume the default position is 0. There is no support for seeking to the default position
|
||||
// in RemoteMediaClient.
|
||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||
if (mediaStatus != null) {
|
||||
if (getCurrentWindowIndex() != windowIndex) {
|
||||
remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,
|
||||
positionMs, null).setResultCallback(seekResultCallback);
|
||||
} else {
|
||||
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
|
||||
}
|
||||
pendingSeekCount++;
|
||||
pendingSeekWindowIndex = windowIndex;
|
||||
pendingSeekPositionMs = positionMs;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
||||
}
|
||||
} else if (pendingSeekCount == 0) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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() {
|
||||
stop(/* reset= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
playbackState = STATE_IDLE;
|
||||
if (remoteMediaClient != null) {
|
||||
// TODO(b/69792021): Support or emulate stop without position reset.
|
||||
remoteMediaClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
|
||||
sessionManager.endCurrentSession(false);
|
||||
}
|
||||
|
||||
@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) {
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RepeatMode public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
||||
// TODO: Support shuffle mode.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getShuffleModeEnabled() {
|
||||
// TODO: Support shuffle mode.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackSelectionArray getCurrentTrackSelections() {
|
||||
return currentTrackSelection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getCurrentTrackGroups() {
|
||||
return currentTrackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline getCurrentTimeline() {
|
||||
return currentTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable public Object getCurrentManifest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
return getCurrentWindowIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentWindowIndex() {
|
||||
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex() {
|
||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
||||
: currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex() {
|
||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
||||
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||
}
|
||||
|
||||
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
||||
// See [Internal: b/65152553].
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return currentTimeline.isEmpty() ? C.TIME_UNSET
|
||||
: currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return pendingSeekPositionMs != C.TIME_UNSET
|
||||
? pendingSeekPositionMs
|
||||
: remoteMediaClient != null
|
||||
? remoteMediaClient.getApproximateStreamPosition()
|
||||
: lastReportedPositionMs;
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
public void updateInternalState() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return;
|
||||
}
|
||||
|
||||
int playbackState = fetchPlaybackState(remoteMediaClient);
|
||||
boolean playWhenReady = !remoteMediaClient.isPaused();
|
||||
if (this.playbackState != playbackState
|
||||
|| this.playWhenReady != playWhenReady) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady = playWhenReady;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
|
||||
}
|
||||
}
|
||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
}
|
||||
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||
}
|
||||
}
|
||||
if (updateTracksAndSelections()) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
||||
}
|
||||
}
|
||||
maybeUpdateTimelineAndNotify();
|
||||
}
|
||||
|
||||
private void maybeUpdateTimelineAndNotify() {
|
||||
if (updateTimeline()) {
|
||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
waitingForInitialTimeline = false;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTimelineChanged(currentTimeline, null, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current timeline and returns whether it has changed.
|
||||
*/
|
||||
private boolean updateTimeline() {
|
||||
CastTimeline oldTimeline = currentTimeline;
|
||||
MediaStatus status = getMediaStatus();
|
||||
currentTimeline =
|
||||
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
return !oldTimeline.equals(currentTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal tracks and selection and returns whether they have changed.
|
||||
*/
|
||||
private boolean updateTracksAndSelections() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return false;
|
||||
}
|
||||
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
|
||||
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
|
||||
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
|
||||
boolean hasChanged = !currentTrackGroups.isEmpty();
|
||||
currentTrackGroups = TrackGroupArray.EMPTY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
return hasChanged;
|
||||
}
|
||||
long[] activeTrackIds = mediaStatus.getActiveTrackIds();
|
||||
if (activeTrackIds == null) {
|
||||
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
|
||||
}
|
||||
|
||||
TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];
|
||||
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
|
||||
for (int i = 0; i < castMediaTracks.size(); i++) {
|
||||
MediaTrack mediaTrack = castMediaTracks.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);
|
||||
}
|
||||
}
|
||||
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
|
||||
TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
|
||||
|
||||
if (!newTrackGroups.equals(currentTrackGroups)
|
||||
|| !newTrackSelections.equals(currentTrackSelection)) {
|
||||
currentTrackSelection = new TrackSelectionArray(trackSelections);
|
||||
currentTrackGroups = new TrackGroupArray(trackGroups);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
updateInternalState();
|
||||
} else {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MediaStatus getMediaStatus() {
|
||||
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}
|
||||
* state
|
||||
*/
|
||||
private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a
|
||||
* {@link Player.RepeatMode}.
|
||||
*/
|
||||
@RepeatMode
|
||||
private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
|
||||
MediaStatus mediaStatus = remoteMediaClient.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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
|
||||
* there is no media session, returns 0.
|
||||
*/
|
||||
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
|
||||
Integer currentItemId = mediaStatus != null
|
||||
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
|
||||
return currentItemId != null ? currentItemId : 0;
|
||||
}
|
||||
|
||||
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 static int getCastRepeatMode(@RepeatMode int repeatMode) {
|
||||
switch (repeatMode) {
|
||||
case REPEAT_MODE_ONE:
|
||||
return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
|
||||
case REPEAT_MODE_ALL:
|
||||
return MediaStatus.REPEAT_MODE_REPEAT_ALL;
|
||||
case REPEAT_MODE_OFF:
|
||||
return MediaStatus.REPEAT_MODE_REPEAT_OFF;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
updateInternalState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdated() {}
|
||||
|
||||
@Override
|
||||
public void onQueueStatusUpdated() {
|
||||
maybeUpdateTimelineAndNotify();
|
||||
}
|
||||
|
||||
@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 SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(@NonNull MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
|
||||
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
if (--pendingSeekCount == 0) {
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.util.SparseIntArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link Timeline} for Cast media queues.
|
||||
*/
|
||||
/* package */ final class CastTimeline extends Timeline {
|
||||
|
||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||
new CastTimeline(
|
||||
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
|
||||
|
||||
private final SparseIntArray idsToIndex;
|
||||
private final int[] ids;
|
||||
private final long[] durationsUs;
|
||||
private final long[] defaultPositionsUs;
|
||||
|
||||
/**
|
||||
* @param items A list of cast media queue items to represent.
|
||||
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
|
||||
*/
|
||||
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
|
||||
int itemCount = items.size();
|
||||
int index = 0;
|
||||
idsToIndex = new SparseIntArray(itemCount);
|
||||
ids = new int[itemCount];
|
||||
durationsUs = new long[itemCount];
|
||||
defaultPositionsUs = new long[itemCount];
|
||||
for (MediaQueueItem item : items) {
|
||||
int itemId = item.getItemId();
|
||||
ids[index] = itemId;
|
||||
idsToIndex.put(itemId, index);
|
||||
MediaInfo mediaInfo = item.getMedia();
|
||||
String contentId = mediaInfo.getContentId();
|
||||
durationsUs[index] =
|
||||
contentIdToDurationUsMap.containsKey(contentId)
|
||||
? contentIdToDurationUsMap.get(contentId)
|
||||
: CastUtils.getStreamDurationUs(mediaInfo);
|
||||
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline implementation.
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
long durationUs = durationsUs[windowIndex];
|
||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
|
||||
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
int id = ids[periodIndex];
|
||||
return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
// equals and hashCode implementations.
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (!(other instanceof CastTimeline)) {
|
||||
return false;
|
||||
}
|
||||
CastTimeline that = (CastTimeline) other;
|
||||
return Arrays.equals(ids, that.ids)
|
||||
&& Arrays.equals(durationsUs, that.durationsUs)
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(ids);
|
||||
result = 31 * result + Arrays.hashCode(durationsUs);
|
||||
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (C) 2018 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.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Creates {@link CastTimeline}s from cast receiver app media status.
|
||||
*
|
||||
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
||||
* durations in the media queue items [See internal: b/65152553].
|
||||
*/
|
||||
/* package */ final class CastTimelineTracker {
|
||||
|
||||
private final HashMap<String, Long> contentIdToDurationUsMap;
|
||||
private final HashSet<String> scratchContentIdSet;
|
||||
|
||||
public CastTimelineTracker() {
|
||||
contentIdToDurationUsMap = new HashMap<>();
|
||||
scratchContentIdSet = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CastTimeline} that represent the given {@code status}.
|
||||
*
|
||||
* @param status The Cast media status.
|
||||
* @return A {@link CastTimeline} that represent the given {@code status}.
|
||||
*/
|
||||
public CastTimeline getCastTimeline(MediaStatus status) {
|
||||
MediaInfo mediaInfo = status.getMediaInfo();
|
||||
List<MediaQueueItem> items = status.getQueueItems();
|
||||
removeUnusedDurationEntries(items);
|
||||
|
||||
if (mediaInfo != null) {
|
||||
String contentId = mediaInfo.getContentId();
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
||||
contentIdToDurationUsMap.put(contentId, durationUs);
|
||||
}
|
||||
return new CastTimeline(items, contentIdToDurationUsMap);
|
||||
}
|
||||
|
||||
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
|
||||
scratchContentIdSet.clear();
|
||||
for (MediaQueueItem item : items) {
|
||||
scratchContentIdSet.add(item.getMedia().getContentId());
|
||||
}
|
||||
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
|
||||
/**
|
||||
* Utility methods for ExoPlayer/Cast integration.
|
||||
*/
|
||||
/* package */ final class CastUtils {
|
||||
|
||||
/**
|
||||
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
|
||||
* unknown or not applicable.
|
||||
*
|
||||
* @param mediaInfo The media info to get the duration from.
|
||||
* @return The duration in microseconds.
|
||||
*/
|
||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||
long durationMs =
|
||||
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
|
||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
switch (statusCode) {
|
||||
case CastStatusCodes.APPLICATION_NOT_FOUND:
|
||||
return "A requested application could not be found.";
|
||||
case CastStatusCodes.APPLICATION_NOT_RUNNING:
|
||||
return "A requested application is not currently running.";
|
||||
case CastStatusCodes.AUTHENTICATION_FAILED:
|
||||
return "Authentication failure.";
|
||||
case CastStatusCodes.CANCELED:
|
||||
return "An in-progress request has been canceled, most likely because another action has "
|
||||
+ "preempted it.";
|
||||
case CastStatusCodes.ERROR_SERVICE_CREATION_FAILED:
|
||||
return "The Cast Remote Display service could not be created.";
|
||||
case CastStatusCodes.ERROR_SERVICE_DISCONNECTED:
|
||||
return "The Cast Remote Display service was disconnected.";
|
||||
case CastStatusCodes.FAILED:
|
||||
return "The in-progress request failed.";
|
||||
case CastStatusCodes.INTERNAL_ERROR:
|
||||
return "An internal error has occurred.";
|
||||
case CastStatusCodes.INTERRUPTED:
|
||||
return "A blocking call was interrupted while waiting and did not run to completion.";
|
||||
case CastStatusCodes.INVALID_REQUEST:
|
||||
return "An invalid request was made.";
|
||||
case CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL:
|
||||
return "A message could not be sent because there is not enough room in the send buffer at "
|
||||
+ "this time.";
|
||||
case CastStatusCodes.MESSAGE_TOO_LARGE:
|
||||
return "A message could not be sent because it is too large.";
|
||||
case CastStatusCodes.NETWORK_ERROR:
|
||||
return "Network I/O error.";
|
||||
case CastStatusCodes.NOT_ALLOWED:
|
||||
return "The request was disallowed and could not be completed.";
|
||||
case CastStatusCodes.REPLACED:
|
||||
return "The request's progress is no longer being tracked because another request of the "
|
||||
+ "same type has been made before the first request completed.";
|
||||
case CastStatusCodes.SUCCESS:
|
||||
return "Success.";
|
||||
case CastStatusCodes.TIMEOUT:
|
||||
return "An operation has timed out.";
|
||||
case CastStatusCodes.UNKNOWN_ERROR:
|
||||
return "An unknown, unexpected error has occurred.";
|
||||
default:
|
||||
return "Unknown: " + statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright (C) 2018 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.C;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.ArrayList;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** Tests for {@link CastTimelineTracker}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
|
||||
public class CastTimelineTrackerTest {
|
||||
|
||||
private static final long DURATION_1_MS = 1000;
|
||||
private static final long DURATION_2_MS = 2000;
|
||||
private static final long DURATION_3_MS = 3000;
|
||||
private static final long DURATION_4_MS = 4000;
|
||||
private static final long DURATION_5_MS = 5000;
|
||||
|
||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||
@Test
|
||||
public void testGetCastTimeline() {
|
||||
MediaInfo mediaInfo;
|
||||
MediaStatus status =
|
||||
mockMediaStatus(
|
||||
new int[] {1, 2, 3},
|
||||
new String[] {"contentId1", "contentId2", "contentId3"},
|
||||
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
|
||||
|
||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||
mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_2_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
MediaStatus newStatus =
|
||||
mockMediaStatus(
|
||||
new int[] {4, 1, 5, 3},
|
||||
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
|
||||
new long[] {
|
||||
MediaInfo.UNKNOWN_DURATION,
|
||||
MediaInfo.UNKNOWN_DURATION,
|
||||
DURATION_5_MS,
|
||||
MediaInfo.UNKNOWN_DURATION
|
||||
});
|
||||
mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.msToUs(DURATION_4_MS),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
}
|
||||
|
||||
private static MediaStatus mockMediaStatus(
|
||||
int[] itemIds, String[] contentIds, long[] durationsMs) {
|
||||
ArrayList<MediaQueueItem> items = new ArrayList<>();
|
||||
for (int i = 0; i < contentIds.length; i++) {
|
||||
MediaInfo mediaInfo = mockMediaInfo(contentIds[i], durationsMs[i]);
|
||||
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
|
||||
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
|
||||
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
|
||||
items.add(item);
|
||||
}
|
||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||
Mockito.when(status.getQueueItems()).thenReturn(items);
|
||||
return status;
|
||||
}
|
||||
|
||||
private static MediaInfo mockMediaInfo(String contentId, long durationMs) {
|
||||
MediaInfo mediaInfo = Mockito.mock(MediaInfo.class);
|
||||
Mockito.when(mediaInfo.getContentId()).thenReturn(contentId);
|
||||
Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs);
|
||||
return mediaInfo;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,20 @@ and enable the extension:
|
|||
1. Copy the three jar files into the `libs` directory of this extension
|
||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
||||
directory of this extension
|
||||
|
||||
* In your `settings.gradle` file, add
|
||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
||||
applies `core_settings.gradle`.
|
||||
1. In your `settings.gradle` file, add
|
||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
||||
applies `core_settings.gradle`.
|
||||
1. In all `build.gradle` files where this extension is linked as a dependency,
|
||||
add
|
||||
```
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
```
|
||||
to enable Java 8 features required by the Cronet library.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
package="com.google.android.exoplayer2.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
|
||||
<instrumentation
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
|
||||
android:targetPackage="com.google.android.exoplayer2.ext.cronet"/>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
|
|
@ -53,13 +52,13 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
|
||||
@Test
|
||||
public void testGetLength() {
|
||||
assertEquals(TEST_DATA.length, byteArrayUploadDataProvider.getLength());
|
||||
assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFullBuffer() throws IOException {
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -69,12 +68,12 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
|
||||
// Read half of the data.
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(firstHalf, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(firstHalf);
|
||||
|
||||
// Read the second half of the data.
|
||||
byteBuffer.rewind();
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(secondHalf, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(secondHalf);
|
||||
verify(mockUploadDataSink, times(2)).onReadSucceeded(false);
|
||||
}
|
||||
|
||||
|
|
@ -82,13 +81,13 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
public void testRewind() throws IOException {
|
||||
// Read all the data.
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
|
||||
// Rewind and make sure it can be read again.
|
||||
byteBuffer.clear();
|
||||
byteArrayUploadDataProvider.rewind(mockUploadDataSink);
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
verify(mockUploadDataSink).onRewindSucceeded();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
|
|
@ -222,7 +219,7 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRequestOpen() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
|
||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +231,7 @@ public final class CronetDataSourceTest {
|
|||
testResponseHeader.put("Content-Length", Long.toString(50L));
|
||||
mockResponseStartSuccess();
|
||||
|
||||
assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec));
|
||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -247,7 +244,7 @@ public final class CronetDataSourceTest {
|
|||
fail("HttpDataSource.HttpDataSourceException expected");
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Check for connection not automatically closed.
|
||||
assertFalse(e.getCause() instanceof UnknownHostException);
|
||||
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
|
@ -264,7 +261,7 @@ public final class CronetDataSourceTest {
|
|||
fail("HttpDataSource.HttpDataSourceException expected");
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Check for connection not automatically closed.
|
||||
assertTrue(e.getCause() instanceof UnknownHostException);
|
||||
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
|
@ -279,7 +276,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail("HttpDataSource.HttpDataSourceException expected");
|
||||
} catch (HttpDataSourceException e) {
|
||||
assertTrue(e instanceof HttpDataSource.InvalidResponseCodeException);
|
||||
assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
|
|
@ -295,7 +292,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail("HttpDataSource.HttpDataSourceException expected");
|
||||
} catch (HttpDataSourceException e) {
|
||||
assertTrue(e instanceof HttpDataSource.InvalidContentTypeException);
|
||||
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
|
||||
|
|
@ -307,7 +304,7 @@ public final class CronetDataSourceTest {
|
|||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
|
||||
assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -346,13 +343,13 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
||||
assertEquals(8, bytesRead);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
|
||||
returnedBuffer = new byte[8];
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertArrayEquals(buildTestDataArray(8, 8), returnedBuffer);
|
||||
assertEquals(8, bytesRead);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(8, 8));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
|
||||
// Should have only called read on cronet once.
|
||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
|
|
@ -378,11 +375,11 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.open(testDataSpec);
|
||||
returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||
assertEquals(10, bytesRead);
|
||||
assertThat(bytesRead).isEqualTo(10);
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||
assertEquals(6, bytesRead);
|
||||
assertThat(bytesRead).isEqualTo(6);
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||
assertEquals(C.RESULT_END_OF_INPUT, bytesRead);
|
||||
assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -394,8 +391,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||
assertEquals(8, bytesRead);
|
||||
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
||||
}
|
||||
|
||||
|
|
@ -410,8 +407,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||
assertEquals(16, bytesRead);
|
||||
assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||
}
|
||||
|
||||
|
|
@ -426,8 +423,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||
assertEquals(16, bytesRead);
|
||||
assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||
}
|
||||
|
||||
|
|
@ -441,8 +438,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
|
||||
assertEquals(8, bytesRead);
|
||||
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
||||
}
|
||||
|
||||
|
|
@ -455,8 +452,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[24];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(0, 16), 24), returnedBuffer);
|
||||
assertEquals(16, bytesRead);
|
||||
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||
}
|
||||
|
||||
|
|
@ -470,8 +467,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
||||
assertEquals(8, bytesRead);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
|
||||
dataSourceUnderTest.close();
|
||||
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
|
||||
|
|
@ -484,7 +481,7 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
// 16 bytes were attempted but only 8 should have been successfully read.
|
||||
assertEquals(8, bytesRead);
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -498,20 +495,20 @@ public final class CronetDataSourceTest {
|
|||
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertEquals(8, bytesRead);
|
||||
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
|
||||
|
||||
// The current buffer is kept if not completely consumed by DataSource reader.
|
||||
returnedBuffer = new byte[8];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
|
||||
assertEquals(14, bytesRead);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(14);
|
||||
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(8, 6), 8));
|
||||
|
||||
// 2 bytes left at this point.
|
||||
returnedBuffer = new byte[8];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertEquals(16, bytesRead);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(14, 2), 8));
|
||||
|
||||
// Should have only called read on cronet once.
|
||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
|
|
@ -524,8 +521,8 @@ public final class CronetDataSourceTest {
|
|||
// Return C.RESULT_END_OF_INPUT
|
||||
returnedBuffer = new byte[16];
|
||||
int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||
assertEquals(C.RESULT_END_OF_INPUT, bytesOverRead);
|
||||
assertArrayEquals(new byte[16], returnedBuffer);
|
||||
assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||
assertThat(returnedBuffer).isEqualTo(new byte[16]);
|
||||
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
||||
verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest,
|
||||
C.RESULT_END_OF_INPUT);
|
||||
|
|
@ -533,7 +530,7 @@ public final class CronetDataSourceTest {
|
|||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
assertEquals(16, bytesRead);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -550,11 +547,10 @@ public final class CronetDataSourceTest {
|
|||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertTrue(e instanceof CronetDataSource.OpenException);
|
||||
assertTrue(e.getCause() instanceof SocketTimeoutException);
|
||||
assertEquals(
|
||||
TEST_CONNECTION_STATUS,
|
||||
((CronetDataSource.OpenException) e).cronetConnectionStatus);
|
||||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
||||
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
|
||||
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
||||
.isEqualTo(TEST_CONNECTION_STATUS);
|
||||
timedOutCondition.open();
|
||||
}
|
||||
}
|
||||
|
|
@ -562,10 +558,10 @@ public final class CronetDataSourceTest {
|
|||
startCondition.block();
|
||||
|
||||
// We should still be trying to open.
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// Now we timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS);
|
||||
timedOutCondition.block();
|
||||
|
|
@ -588,11 +584,10 @@ public final class CronetDataSourceTest {
|
|||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertTrue(e instanceof CronetDataSource.OpenException);
|
||||
assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
|
||||
assertEquals(
|
||||
TEST_INVALID_CONNECTION_STATUS,
|
||||
((CronetDataSource.OpenException) e).cronetConnectionStatus);
|
||||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
||||
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
||||
.isEqualTo(TEST_INVALID_CONNECTION_STATUS);
|
||||
timedOutCondition.open();
|
||||
}
|
||||
}
|
||||
|
|
@ -601,10 +596,10 @@ public final class CronetDataSourceTest {
|
|||
startCondition.block();
|
||||
|
||||
// We should still be trying to open.
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutCondition.block();
|
||||
|
|
@ -632,10 +627,10 @@ public final class CronetDataSourceTest {
|
|||
startCondition.block();
|
||||
|
||||
// We should still be trying to open.
|
||||
assertFalse(openCondition.block(50));
|
||||
assertThat(openCondition.block(50)).isFalse();
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertFalse(openCondition.block(50));
|
||||
assertThat(openCondition.block(50)).isFalse();
|
||||
// The response arrives just in time.
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
openCondition.block();
|
||||
|
|
@ -656,8 +651,8 @@ public final class CronetDataSourceTest {
|
|||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertTrue(e instanceof CronetDataSource.OpenException);
|
||||
assertTrue(e.getCause() instanceof SocketTimeoutException);
|
||||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
||||
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
|
||||
openExceptions.getAndIncrement();
|
||||
timedOutCondition.open();
|
||||
}
|
||||
|
|
@ -666,10 +661,10 @@ public final class CronetDataSourceTest {
|
|||
startCondition.block();
|
||||
|
||||
// We should still be trying to open.
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// A redirect arrives just in time.
|
||||
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
|
||||
"RandomRedirectedUrl1");
|
||||
|
|
@ -677,9 +672,9 @@ public final class CronetDataSourceTest {
|
|||
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
|
||||
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
|
||||
// Give the thread some time to run.
|
||||
assertFalse(timedOutCondition.block(newTimeoutMs));
|
||||
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
|
||||
// We should still be trying to open as we approach the new timeout.
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// A redirect arrives just in time.
|
||||
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
|
||||
"RandomRedirectedUrl2");
|
||||
|
|
@ -687,15 +682,15 @@ public final class CronetDataSourceTest {
|
|||
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
||||
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
|
||||
// Give the thread some time to run.
|
||||
assertFalse(timedOutCondition.block(newTimeoutMs));
|
||||
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
|
||||
// We should still be trying to open as we approach the new timeout.
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// Now we timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs);
|
||||
timedOutCondition.block();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
assertEquals(1, openExceptions.get());
|
||||
assertThat(openExceptions.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -855,7 +850,7 @@ public final class CronetDataSourceTest {
|
|||
fail();
|
||||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
|
||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
||||
timedOutCondition.open();
|
||||
}
|
||||
}
|
||||
|
|
@ -863,7 +858,7 @@ public final class CronetDataSourceTest {
|
|||
thread.start();
|
||||
startCondition.block();
|
||||
|
||||
assertFalse(timedOutCondition.block(50));
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutCondition.block();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
package="com.google.android.exoplayer2.ext.cronet">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
throw new HttpDataSourceException(exception, currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
} else if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
|
|
|
|||
|
|
@ -69,18 +69,23 @@ import java.util.List;
|
|||
}
|
||||
|
||||
@Override
|
||||
public DecoderInputBuffer createInputBuffer() {
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleOutputBuffer createOutputBuffer() {
|
||||
protected SimpleOutputBuffer createOutputBuffer() {
|
||||
return new SimpleOutputBuffer(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer,
|
||||
SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) {
|
||||
return new FfmpegDecoderException("Unexpected decode error", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
nativeContext = ffmpegReset(nativeContext, extraData);
|
||||
if (nativeContext == 0) {
|
||||
|
|
|
|||
|
|
@ -26,4 +26,7 @@ public final class FfmpegDecoderException extends AudioDecoderException {
|
|||
super(message);
|
||||
}
|
||||
|
||||
/* package */ FfmpegDecoderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -25,6 +25,7 @@ track 0:
|
|||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 526272
|
||||
sample count = 33
|
||||
sample 0:
|
||||
time = 0
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -25,6 +25,7 @@ track 0:
|
|||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 362432
|
||||
sample count = 23
|
||||
sample 0:
|
||||
time = 853333
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -25,6 +25,7 @@ track 0:
|
|||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 182208
|
||||
sample count = 12
|
||||
sample 0:
|
||||
time = 1792000
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -25,6 +25,7 @@ track 0:
|
|||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 18368
|
||||
sample count = 2
|
||||
sample 0:
|
||||
time = 2645333
|
||||
|
|
|
|||
|
|
@ -34,11 +34,14 @@ public class FlacExtractorTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
public void testSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlacExtractor();
|
||||
}
|
||||
}, "bear.flac", getInstrumentation());
|
||||
ExtractorAsserts.assertBehavior(
|
||||
new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlacExtractor();
|
||||
}
|
||||
},
|
||||
"bear.flac",
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,18 +70,23 @@ import java.util.List;
|
|||
}
|
||||
|
||||
@Override
|
||||
public DecoderInputBuffer createInputBuffer() {
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleOutputBuffer createOutputBuffer() {
|
||||
protected SimpleOutputBuffer createOutputBuffer() {
|
||||
return new SimpleOutputBuffer(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlacDecoderException decode(DecoderInputBuffer inputBuffer,
|
||||
SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
protected FlacDecoderException createUnexpectedDecodeException(Throwable error) {
|
||||
return new FlacDecoderException("Unexpected decode error", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FlacDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
decoderJni.flush();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,7 @@ public final class FlacDecoderException extends AudioDecoderException {
|
|||
super(message);
|
||||
}
|
||||
|
||||
/* package */ FlacDecoderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor {
|
|||
}
|
||||
metadataParsed = true;
|
||||
|
||||
extractorOutput.seekMap(new SeekMap() {
|
||||
final boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
|
||||
final long durationUs = streamInfo.durationUs();
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return isSeekable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return durationUs;
|
||||
}
|
||||
|
||||
});
|
||||
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
|
||||
extractorOutput.seekMap(
|
||||
isSeekable
|
||||
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
|
||||
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
null,
|
||||
|
|
@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class FlacSeekMap implements SeekMap {
|
||||
|
||||
private final long durationUs;
|
||||
private final FlacDecoderJni decoderJni;
|
||||
|
||||
public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
|
||||
this.durationUs = durationUs;
|
||||
this.decoderJni = decoderJni;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekPoints getSeekPoints(long timeUs) {
|
||||
// TODO: Access the seek table via JNI to return two seek points when appropriate.
|
||||
return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return durationUs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ class JavaDataSource : public DataSource {
|
|||
ssize_t readAt(off64_t offset, void *const data, size_t size) {
|
||||
jobject byteBuffer = env->NewDirectByteBuffer(data, size);
|
||||
int result = env->CallIntMethod(flacDecoderJni, mid, byteBuffer);
|
||||
if (env->ExceptionOccurred()) {
|
||||
if (env->ExceptionCheck()) {
|
||||
// Exception is thrown in Java when returning from the native call.
|
||||
result = -1;
|
||||
}
|
||||
env->DeleteLocalRef(byteBuffer);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
class DataSource {
|
||||
public:
|
||||
virtual ~DataSource() {}
|
||||
// Returns the number of bytes read, or -1 on failure. It's not an error if
|
||||
// this returns zero; it just means the given offset is equal to, or
|
||||
// beyond, the end of the source.
|
||||
|
|
|
|||
6
extensions/ima/proguard-rules.txt
Normal file
6
extensions/ima/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Proguard rules specific to the IMA extension.
|
||||
|
||||
-keep class com.google.ads.interactivemedia.** { *; }
|
||||
-keep interface com.google.ads.interactivemedia.** { *; }
|
||||
-keep class com.google.obf.** { *; }
|
||||
-keep interface com.google.obf.** { *; }
|
||||
|
|
@ -25,6 +25,8 @@ import android.view.ViewGroup;
|
|||
import android.webkit.WebView;
|
||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||
import com.google.ads.interactivemedia.v3.api.AdError;
|
||||
import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode;
|
||||
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
|
||||
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
|
||||
import com.google.ads.interactivemedia.v3.api.AdEvent;
|
||||
|
|
@ -48,6 +50,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
|||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -160,6 +163,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
*/
|
||||
private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
|
||||
|
||||
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
|
||||
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
|
||||
|
||||
/**
|
||||
* The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be
|
||||
* clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in
|
||||
|
|
@ -211,6 +217,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
|
||||
// Fields tracking IMA's state.
|
||||
|
||||
/** The expected ad group index that IMA should load next. */
|
||||
private int expectedAdGroupIndex;
|
||||
/**
|
||||
* The index of the current ad group that IMA is loading.
|
||||
*/
|
||||
|
|
@ -239,9 +247,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
*/
|
||||
private int playingAdIndexInAdGroup;
|
||||
/**
|
||||
* If a content period has finished but IMA has not yet sent an ad event with
|
||||
* {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
|
||||
* {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
|
||||
* Whether there's a pending ad preparation error which IMA needs to be notified of when it
|
||||
* transitions from playing content to playing the ad.
|
||||
*/
|
||||
private boolean shouldNotifyAdPrepareError;
|
||||
/**
|
||||
* If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value
|
||||
* of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
|
||||
* determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
|
||||
*/
|
||||
private long fakeContentProgressElapsedRealtimeMs;
|
||||
|
|
@ -332,7 +344,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
*
|
||||
* <p>Ads will be requested automatically when the player is prepared if this method has not been
|
||||
* called, so it is only necessary to call this method if you want to request ads before preparing
|
||||
* the player
|
||||
* the player.
|
||||
*
|
||||
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
|
||||
*/
|
||||
|
|
@ -373,7 +385,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
|
||||
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
|
||||
} else if (contentType == C.TYPE_SS) {
|
||||
// IMA does not support SmoothStreaming ad media.
|
||||
// IMA does not support Smooth Streaming ad media.
|
||||
}
|
||||
}
|
||||
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
|
||||
|
|
@ -388,10 +400,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
lastContentProgress = null;
|
||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||
player.addListener(this);
|
||||
maybeNotifyAdError();
|
||||
maybeNotifyPendingAdLoadError();
|
||||
if (adPlaybackState != null) {
|
||||
// Pass the ad playback state to the player, and resume ads if necessary.
|
||||
eventListener.onAdPlaybackState(adPlaybackState.copy());
|
||||
eventListener.onAdPlaybackState(adPlaybackState);
|
||||
if (imaPausedContent && player.getPlayWhenReady()) {
|
||||
adsManager.resume();
|
||||
}
|
||||
|
|
@ -407,7 +419,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
@Override
|
||||
public void detachPlayer() {
|
||||
if (adsManager != null && imaPausedContent) {
|
||||
adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAdResumePositionUs(
|
||||
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
|
||||
adsManager.pause();
|
||||
}
|
||||
lastAdProgress = getAdProgress();
|
||||
|
|
@ -427,6 +441,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception);
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("handlePrepareError", e);
|
||||
}
|
||||
}
|
||||
|
||||
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
|
||||
|
||||
@Override
|
||||
|
|
@ -442,7 +468,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
adsManager.addAdEventListener(this);
|
||||
if (player != null) {
|
||||
// If a player is attached already, start playback immediately.
|
||||
startAdPlayback();
|
||||
try {
|
||||
startAdPlayback();
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("onAdsManagerLoaded", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,65 +481,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
@Override
|
||||
public void onAdEvent(AdEvent adEvent) {
|
||||
AdEventType adEventType = adEvent.getType();
|
||||
boolean isLogAdEvent = adEventType == AdEventType.LOG;
|
||||
if (DEBUG || isLogAdEvent) {
|
||||
Log.w(TAG, "onAdEvent: " + adEventType);
|
||||
if (isLogAdEvent) {
|
||||
for (Map.Entry<String, String> entry : adEvent.getAdData().entrySet()) {
|
||||
Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue());
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onAdEvent: " + adEventType);
|
||||
}
|
||||
if (adsManager == null) {
|
||||
Log.w(TAG, "Dropping ad event after release: " + adEvent);
|
||||
return;
|
||||
}
|
||||
Ad ad = adEvent.getAd();
|
||||
switch (adEvent.getType()) {
|
||||
case LOADED:
|
||||
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
|
||||
AdPodInfo adPodInfo = ad.getAdPodInfo();
|
||||
int podIndex = adPodInfo.getPodIndex();
|
||||
adGroupIndex =
|
||||
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
|
||||
int adPosition = adPodInfo.getAdPosition();
|
||||
int adCount = adPodInfo.getTotalAds();
|
||||
adsManager.start();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
|
||||
}
|
||||
adPlaybackState.setAdCount(adGroupIndex, adCount);
|
||||
updateAdPlaybackState();
|
||||
break;
|
||||
case CONTENT_PAUSE_REQUESTED:
|
||||
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
|
||||
// before sending CONTENT_RESUME_REQUESTED.
|
||||
imaPausedContent = true;
|
||||
pauseContentInternal();
|
||||
break;
|
||||
case STARTED:
|
||||
if (ad.isSkippable()) {
|
||||
focusSkipButton();
|
||||
}
|
||||
break;
|
||||
case TAPPED:
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdTapped();
|
||||
}
|
||||
break;
|
||||
case CLICKED:
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdClicked();
|
||||
}
|
||||
break;
|
||||
case CONTENT_RESUME_REQUESTED:
|
||||
imaPausedContent = false;
|
||||
resumeContentInternal();
|
||||
break;
|
||||
case ALL_ADS_COMPLETED:
|
||||
// Do nothing. The ads manager will be released when the source is released.
|
||||
default:
|
||||
break;
|
||||
try {
|
||||
handleAdEvent(adEvent);
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("onAdEvent", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -517,41 +499,68 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
|
||||
@Override
|
||||
public void onAdError(AdErrorEvent adErrorEvent) {
|
||||
AdError error = adErrorEvent.getError();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onAdError " + adErrorEvent);
|
||||
Log.d(TAG, "onAdError", error);
|
||||
}
|
||||
if (adsManager == null) {
|
||||
// No ads were loaded, so allow playback to start without any ads.
|
||||
pendingAdRequestContext = null;
|
||||
adPlaybackState = new AdPlaybackState(new long[0]);
|
||||
adPlaybackState = new AdPlaybackState();
|
||||
updateAdPlaybackState();
|
||||
} else if (isAdGroupLoadError(error)) {
|
||||
try {
|
||||
handleAdGroupLoadError();
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("onAdError", e);
|
||||
}
|
||||
}
|
||||
if (pendingAdErrorEvent == null) {
|
||||
pendingAdErrorEvent = adErrorEvent;
|
||||
}
|
||||
maybeNotifyAdError();
|
||||
maybeNotifyPendingAdLoadError();
|
||||
}
|
||||
|
||||
// ContentProgressProvider implementation.
|
||||
|
||||
@Override
|
||||
public VideoProgressUpdate getContentProgress() {
|
||||
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
|
||||
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
|
||||
if (player == null) {
|
||||
return lastContentProgress;
|
||||
} else if (pendingContentPositionMs != C.TIME_UNSET) {
|
||||
}
|
||||
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
|
||||
long contentPositionMs;
|
||||
if (pendingContentPositionMs != C.TIME_UNSET) {
|
||||
sentPendingContentPositionMs = true;
|
||||
return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
|
||||
contentPositionMs = pendingContentPositionMs;
|
||||
expectedAdGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
|
||||
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
|
||||
long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
|
||||
return new VideoProgressUpdate(fakePositionMs, contentDurationMs);
|
||||
} else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) {
|
||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
|
||||
expectedAdGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
|
||||
contentPositionMs = player.getCurrentPosition();
|
||||
// Update the expected ad group index for the current content position. The update is delayed
|
||||
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
|
||||
// just after an ad group isn't incorrectly attributed to the next ad group.
|
||||
int nextAdGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs));
|
||||
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
|
||||
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
|
||||
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
|
||||
nextAdGroupTimeMs = contentDurationMs;
|
||||
}
|
||||
if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) {
|
||||
expectedAdGroupIndex = nextAdGroupIndex;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
|
||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||
}
|
||||
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
|
||||
return new VideoProgressUpdate(contentPositionMs, contentDurationMs);
|
||||
}
|
||||
|
||||
// VideoAdPlayer implementation.
|
||||
|
|
@ -560,22 +569,40 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
public VideoProgressUpdate getAdProgress() {
|
||||
if (player == null) {
|
||||
return lastAdProgress;
|
||||
} else if (imaAdState == IMA_AD_STATE_NONE) {
|
||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||
} else {
|
||||
} else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {
|
||||
long adDuration = player.getDuration();
|
||||
return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY
|
||||
: new VideoProgressUpdate(player.getCurrentPosition(), adDuration);
|
||||
} else {
|
||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAd(String adUriString) {
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Unexpected loadAd without LOADED event; assuming ad group index is actually "
|
||||
+ expectedAdGroupIndex);
|
||||
adGroupIndex = expectedAdGroupIndex;
|
||||
adsManager.start();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
|
||||
}
|
||||
adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString));
|
||||
updateAdPlaybackState();
|
||||
try {
|
||||
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
|
||||
if (adIndexInAdGroup == C.INDEX_UNSET) {
|
||||
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
|
||||
return;
|
||||
}
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
|
||||
updateAdPlaybackState();
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("loadAd", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -600,10 +627,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
Log.w(TAG, "Unexpected playAd without stopAd");
|
||||
break;
|
||||
case IMA_AD_STATE_NONE:
|
||||
// IMA is requesting to play the ad, so stop faking the content position.
|
||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
||||
imaAdState = IMA_AD_STATE_PLAYING;
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPlay();
|
||||
}
|
||||
if (shouldNotifyAdPrepareError) {
|
||||
shouldNotifyAdPrepareError = false;
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onError();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMA_AD_STATE_PAUSED:
|
||||
imaAdState = IMA_AD_STATE_PLAYING;
|
||||
|
|
@ -635,7 +671,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
Log.w(TAG, "Unexpected stopAd");
|
||||
return;
|
||||
}
|
||||
stopAdInternal();
|
||||
try {
|
||||
stopAdInternal();
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("stopAd", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -656,15 +696,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
@Override
|
||||
public void resumeAd() {
|
||||
// This method is never called. See [Internal: b/18931719].
|
||||
throw new IllegalStateException();
|
||||
maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd"));
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
if (timeline.isEmpty()) {
|
||||
// The player is being re-prepared and this source will be released.
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest,
|
||||
@Player.TimelineChangeReason int reason) {
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
|
||||
// The player is being reset and this source will be released.
|
||||
return;
|
||||
}
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
|
|
@ -672,7 +713,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
long contentDurationUs = timeline.getPeriod(0, period).durationUs;
|
||||
contentDurationMs = C.usToMs(contentDurationUs);
|
||||
if (contentDurationUs != C.TIME_UNSET) {
|
||||
adPlaybackState.contentDurationUs = contentDurationUs;
|
||||
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
|
||||
}
|
||||
updateImaStateForPlayerState();
|
||||
}
|
||||
|
|
@ -710,7 +751,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
if (playingAd) {
|
||||
if (imaAdState != IMA_AD_STATE_NONE) {
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onError();
|
||||
}
|
||||
|
|
@ -727,16 +768,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
if (sentContentComplete) {
|
||||
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
|
||||
if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
|
||||
adPlaybackState.playedAdGroup(i);
|
||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
||||
}
|
||||
}
|
||||
updateAdPlaybackState();
|
||||
} else {
|
||||
long positionMs = player.getCurrentPosition();
|
||||
timeline.getPeriod(0, period);
|
||||
if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) {
|
||||
int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
|
||||
if (newAdGroupIndex != C.INDEX_UNSET) {
|
||||
sentPendingContentPositionMs = false;
|
||||
pendingContentPositionMs = positionMs;
|
||||
if (newAdGroupIndex != adGroupIndex) {
|
||||
shouldNotifyAdPrepareError = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -753,21 +798,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
||||
|
||||
// Set up the ad playback state, skipping ads based on the start position as required.
|
||||
pendingContentPositionMs = player.getCurrentPosition();
|
||||
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
|
||||
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
|
||||
long contentPositionMs = player.getCurrentPosition();
|
||||
int adGroupIndexForPosition =
|
||||
getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs));
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||
if (adGroupIndexForPosition == 0) {
|
||||
podIndexOffset = 0;
|
||||
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
|
||||
pendingContentPositionMs = C.TIME_UNSET;
|
||||
// There is no preroll and midroll pod indices start at 1.
|
||||
podIndexOffset = -1;
|
||||
} else /* adGroupIndexForPosition > 0 */ {
|
||||
// Skip ad groups before the one at or immediately before the playback position.
|
||||
for (int i = 0; i < adGroupIndexForPosition; i++) {
|
||||
adPlaybackState.playedAdGroup(i);
|
||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
||||
}
|
||||
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
|
||||
// with rounding one of the two ad times.
|
||||
|
|
@ -781,6 +825,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
podIndexOffset = adGroupIndexForPosition - 1;
|
||||
}
|
||||
|
||||
if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) {
|
||||
// Provide the player's initial position to trigger loading and playing the ad.
|
||||
pendingContentPositionMs = contentPositionMs;
|
||||
}
|
||||
|
||||
// Start ad playback.
|
||||
adsManager.init(adsRenderingSettings);
|
||||
updateAdPlaybackState();
|
||||
|
|
@ -789,12 +838,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyAdError() {
|
||||
if (eventListener != null && pendingAdErrorEvent != null) {
|
||||
IOException exception =
|
||||
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError());
|
||||
eventListener.onLoadError(exception);
|
||||
pendingAdErrorEvent = null;
|
||||
private void handleAdEvent(AdEvent adEvent) {
|
||||
Ad ad = adEvent.getAd();
|
||||
switch (adEvent.getType()) {
|
||||
case LOADED:
|
||||
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
|
||||
AdPodInfo adPodInfo = ad.getAdPodInfo();
|
||||
int podIndex = adPodInfo.getPodIndex();
|
||||
adGroupIndex =
|
||||
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
|
||||
int adPosition = adPodInfo.getAdPosition();
|
||||
int adCount = adPodInfo.getTotalAds();
|
||||
adsManager.start();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
|
||||
}
|
||||
int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count;
|
||||
if (adCount != oldAdCount) {
|
||||
if (oldAdCount == C.LENGTH_UNSET) {
|
||||
adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount);
|
||||
updateAdPlaybackState();
|
||||
} else {
|
||||
// IMA sometimes unexpectedly decreases the ad count in an ad group.
|
||||
Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount);
|
||||
}
|
||||
}
|
||||
if (adGroupIndex != expectedAdGroupIndex) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Expected ad group index "
|
||||
+ expectedAdGroupIndex
|
||||
+ ", actual ad group index "
|
||||
+ adGroupIndex);
|
||||
expectedAdGroupIndex = adGroupIndex;
|
||||
}
|
||||
break;
|
||||
case CONTENT_PAUSE_REQUESTED:
|
||||
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
|
||||
// before sending CONTENT_RESUME_REQUESTED.
|
||||
imaPausedContent = true;
|
||||
pauseContentInternal();
|
||||
break;
|
||||
case STARTED:
|
||||
if (ad.isSkippable()) {
|
||||
focusSkipButton();
|
||||
}
|
||||
break;
|
||||
case TAPPED:
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdTapped();
|
||||
}
|
||||
break;
|
||||
case CLICKED:
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdClicked();
|
||||
}
|
||||
break;
|
||||
case CONTENT_RESUME_REQUESTED:
|
||||
imaPausedContent = false;
|
||||
resumeContentInternal();
|
||||
break;
|
||||
case LOG:
|
||||
Map<String, String> adData = adEvent.getAdData();
|
||||
Log.i(TAG, "Log AdEvent: " + adData);
|
||||
if ("adLoadError".equals(adData.get("type"))) {
|
||||
handleAdGroupLoadError();
|
||||
}
|
||||
break;
|
||||
case ALL_ADS_COMPLETED:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -815,9 +928,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
|
||||
}
|
||||
}
|
||||
if (!wasPlayingAd && playingAd) {
|
||||
if (!wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
|
||||
int adGroupIndex = player.getCurrentAdGroupIndex();
|
||||
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
|
||||
// IMA hasn't called playAd yet, so fake the content position.
|
||||
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
|
||||
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
|
||||
|
|
@ -834,8 +947,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
|
||||
}
|
||||
}
|
||||
if (playingAd && adGroupIndex != C.INDEX_UNSET) {
|
||||
adPlaybackState.playedAdGroup(adGroupIndex);
|
||||
if (adGroupIndex != C.INDEX_UNSET) {
|
||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
|
||||
adGroupIndex = C.INDEX_UNSET;
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
|
|
@ -847,21 +960,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
pendingContentPositionMs = C.TIME_UNSET;
|
||||
sentPendingContentPositionMs = false;
|
||||
}
|
||||
// IMA is requesting to pause content, so stop faking the content position.
|
||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void stopAdInternal() {
|
||||
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
adPlaybackState.playedAd(adGroupIndex);
|
||||
int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
|
||||
// TODO: Handle the skipped event so the ad can be marked as skipped rather than played.
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
|
||||
updateAdPlaybackState();
|
||||
if (!playingAd) {
|
||||
adGroupIndex = C.INDEX_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAdGroupLoadError() {
|
||||
int adGroupIndex =
|
||||
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
// Drop the error, as we don't know which ad group it relates to.
|
||||
return;
|
||||
}
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
|
||||
if (adGroup.count == C.LENGTH_UNSET) {
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length));
|
||||
adGroup = adPlaybackState.adGroups[adGroupIndex];
|
||||
}
|
||||
for (int i = 0; i < adGroup.count; i++) {
|
||||
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
|
||||
}
|
||||
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
|
||||
}
|
||||
}
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
|
||||
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
|
||||
}
|
||||
if (imaAdState == IMA_AD_STATE_NONE) {
|
||||
// Send IMA a content position at the ad group so that it will try to play it, at which point
|
||||
// we can notify that it failed to load.
|
||||
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
|
||||
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
|
||||
fakeContentProgressOffsetMs = contentDurationMs;
|
||||
}
|
||||
shouldNotifyAdPrepareError = true;
|
||||
} else {
|
||||
// We're already playing an ad.
|
||||
if (adIndexInAdGroup > playingAdIndexInAdGroup) {
|
||||
// Mark the playing ad as ended so we can notify the error on the next ad and remove it,
|
||||
// which means that the ad after will load (if any).
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
}
|
||||
playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onError();
|
||||
}
|
||||
}
|
||||
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
|
||||
private void checkForContentComplete() {
|
||||
if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET
|
||||
&& player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
|
||||
|
|
@ -877,7 +1045,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
private void updateAdPlaybackState() {
|
||||
// Ignore updates while detached. When a player is attached it will receive the latest state.
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdPlaybackState(adPlaybackState.copy());
|
||||
eventListener.onAdPlaybackState(adPlaybackState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -890,6 +1058,48 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
|
||||
* ads in the ad group have loaded.
|
||||
*/
|
||||
private int getAdIndexInAdGroupToLoad(int adGroupIndex) {
|
||||
@AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states;
|
||||
int adIndexInAdGroup = 0;
|
||||
// IMA loads ads in order.
|
||||
while (adIndexInAdGroup < states.length
|
||||
&& states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
|
||||
adIndexInAdGroup++;
|
||||
}
|
||||
return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup;
|
||||
}
|
||||
|
||||
private void maybeNotifyPendingAdLoadError() {
|
||||
if (pendingAdErrorEvent != null) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdLoadError(
|
||||
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
|
||||
}
|
||||
pendingAdErrorEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyInternalError(String name, Exception cause) {
|
||||
String message = "Internal error in " + name;
|
||||
Log.e(TAG, message, cause);
|
||||
if (eventListener != null) {
|
||||
eventListener.onInternalAdLoadError(new RuntimeException(message, cause));
|
||||
}
|
||||
// We can't recover from an unexpected error in general, so skip all remaining ads.
|
||||
if (adPlaybackState == null) {
|
||||
adPlaybackState = new AdPlaybackState();
|
||||
} else {
|
||||
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
|
||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
||||
}
|
||||
}
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
|
||||
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
|
||||
if (cuePoints.isEmpty()) {
|
||||
// If no cue points are specified, there is a preroll ad.
|
||||
|
|
@ -898,28 +1108,35 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
|
||||
int count = cuePoints.size();
|
||||
long[] adGroupTimesUs = new long[count];
|
||||
int adGroupIndex = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double cuePoint = cuePoints.get(i);
|
||||
adGroupTimesUs[i] =
|
||||
cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
|
||||
if (cuePoint == -1.0) {
|
||||
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
|
||||
} else {
|
||||
adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint);
|
||||
}
|
||||
}
|
||||
// Cue points may be out of order, so sort them.
|
||||
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
|
||||
return adGroupTimesUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the ad group that should be played before playing the content at {@code
|
||||
* playbackPositionUs} when starting playback for the first time. This is the latest ad group at
|
||||
* or before the specified playback position. If the first ad is after the playback position,
|
||||
* returns {@link C#INDEX_UNSET}.
|
||||
*/
|
||||
private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) {
|
||||
for (int i = 0; i < adGroupTimesUs.length; i++) {
|
||||
long adGroupTimeUs = adGroupTimesUs[i];
|
||||
// A postroll ad is after any position in the content.
|
||||
if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) {
|
||||
return i == 0 ? C.INDEX_UNSET : (i - 1);
|
||||
}
|
||||
private static boolean isAdGroupLoadError(AdError adError) {
|
||||
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
|
||||
// a single ad, ad group or the whole timeline.
|
||||
return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH;
|
||||
}
|
||||
|
||||
private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
|
||||
int count = adGroupTimesUs.length;
|
||||
if (count == 1) {
|
||||
return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE;
|
||||
} else if (count == 2) {
|
||||
return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;
|
||||
} else {
|
||||
// There's at least one midroll ad group, as adGroupTimesUs is never empty.
|
||||
return true;
|
||||
}
|
||||
return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ import android.support.annotation.Nullable;
|
|||
import android.view.ViewGroup;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link MediaSource} that inserts ads linearly with a provided content media source.
|
||||
|
|
@ -33,9 +33,10 @@ import java.io.IOException;
|
|||
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class ImaAdsMediaSource implements MediaSource {
|
||||
public final class ImaAdsMediaSource extends CompositeMediaSource<Void> {
|
||||
|
||||
private final AdsMediaSource adsMediaSource;
|
||||
private Listener listener;
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
|
|
@ -74,20 +75,10 @@ public final class ImaAdsMediaSource implements MediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(final ExoPlayer player, boolean isTopLevelSource,
|
||||
final Listener listener) {
|
||||
adsMediaSource.prepareSource(player, false, new Listener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
|
||||
@Nullable Object manifest) {
|
||||
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
adsMediaSource.maybeThrowSourceInfoRefreshError();
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
super.prepareSource(player, isTopLevelSource, listener);
|
||||
this.listener = listener;
|
||||
prepareChildSource(/* id= */ null, adsMediaSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -101,8 +92,8 @@ public final class ImaAdsMediaSource implements MediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
adsMediaSource.releaseSource();
|
||||
protected void onChildSourceInfoRefreshed(
|
||||
Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
|
||||
listener.onSourceInfoRefreshed(this, timeline, manifest);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
6
extensions/ima/src/main/proguard-rules.txt
Normal file
6
extensions/ima/src/main/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Proguard rules specific to the IMA extension.
|
||||
|
||||
-keep class com.google.ads.interactivemedia.** { *; }
|
||||
-keep interface com.google.ads.interactivemedia.** { *; }
|
||||
-keep class com.google.obf.** { *; }
|
||||
-keep interface com.google.obf.** { *; }
|
||||
|
|
@ -30,7 +30,7 @@ dependencies {
|
|||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Leanback extension for Exoplayer library'
|
||||
javadocTitle = 'Leanback extension'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
|
|
|
|||
|
|
@ -30,15 +30,15 @@ import com.google.android.exoplayer2.ControlDispatcher;
|
|||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
|
||||
/**
|
||||
* Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}.
|
||||
*/
|
||||
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
|
||||
public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||
|
||||
static {
|
||||
|
|
@ -46,11 +46,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
}
|
||||
|
||||
private final Context context;
|
||||
private final SimpleExoPlayer player;
|
||||
private final Player player;
|
||||
private final Handler handler;
|
||||
private final ComponentListener componentListener;
|
||||
private final Runnable updateProgressRunnable;
|
||||
|
||||
private @Nullable PlaybackPreparer playbackPreparer;
|
||||
private ControlDispatcher controlDispatcher;
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||
|
|
@ -59,14 +60,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
|
||||
/**
|
||||
* Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the
|
||||
* {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when
|
||||
* it's no longer required.
|
||||
* {@link Player} instance. The caller remains responsible for releasing the player when it's no
|
||||
* longer required.
|
||||
*
|
||||
* @param context The current context (activity).
|
||||
* @param player Instance of your exoplayer that needs to be configured.
|
||||
* @param updatePeriodMs The delay between player control updates, in milliseconds.
|
||||
*/
|
||||
public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) {
|
||||
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
handler = new Handler();
|
||||
|
|
@ -83,6 +84,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PlaybackPreparer}.
|
||||
*
|
||||
* @param playbackPreparer The {@link PlaybackPreparer}.
|
||||
*/
|
||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||
this.playbackPreparer = playbackPreparer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ControlDispatcher}.
|
||||
*
|
||||
|
|
@ -114,13 +124,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
}
|
||||
notifyStateChanged();
|
||||
player.addListener(componentListener);
|
||||
player.addVideoListener(componentListener);
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.addVideoListener(componentListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromHost() {
|
||||
player.removeListener(componentListener);
|
||||
player.removeVideoListener(componentListener);
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.removeVideoListener(componentListener);
|
||||
}
|
||||
if (surfaceHolderGlueHost != null) {
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
||||
surfaceHolderGlueHost = null;
|
||||
|
|
@ -160,7 +176,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
|
||||
@Override
|
||||
public void play() {
|
||||
if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
if (playbackPreparer != null) {
|
||||
playbackPreparer.preparePlayback();
|
||||
}
|
||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||
}
|
||||
if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) {
|
||||
|
|
@ -195,7 +215,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
|
||||
/* package */ void setVideoSurface(Surface surface) {
|
||||
hasSurface = surface != null;
|
||||
player.setVideoSurface(surface);
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
maybeNotifyPreparedStateChanged(getCallback());
|
||||
}
|
||||
|
||||
|
|
@ -218,8 +241,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
private final class ComponentListener extends Player.DefaultEventListener implements
|
||||
SimpleExoPlayer.VideoListener, SurfaceHolder.Callback {
|
||||
private final class ComponentListener extends Player.DefaultEventListener
|
||||
implements SurfaceHolder.Callback, VideoListener {
|
||||
|
||||
// SurfaceHolder.Callback implementation.
|
||||
|
||||
|
|
@ -258,7 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest,
|
||||
@TimelineChangeReason int reason) {
|
||||
Callback callback = getCallback();
|
||||
callback.onDurationChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
|
|
@ -272,11 +296,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
||||
}
|
||||
|
||||
// SimpleExoplayerView.Callback implementation.
|
||||
// VideoListener implementation.
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
public void onVideoSizeChanged(
|
||||
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -655,7 +655,8 @@ public final class MediaSessionConnector {
|
|||
private int currentWindowCount;
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest,
|
||||
@Player.TimelineChangeReason int reason) {
|
||||
int windowCount = player.getCurrentTimeline().getWindowCount();
|
||||
int windowIndex = player.getCurrentWindowIndex();
|
||||
if (queueNavigator != null) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Herhaal alles"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Herhaal niks"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Herhaal een"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Herhaal niks"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Herhaal een"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Herhaal alles"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"ሁሉንም ድገም"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"ምንም አትድገም"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"አንዱን ድገም"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"ምንም አትድገም"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"አንድ ድገም"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"ሁሉንም ድገም"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"تكرار الكل"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"عدم التكرار"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"تكرار مقطع واحد"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"عدم التكرار"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"تكرار مقطع صوتي واحد"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"تكرار الكل"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Ponovi sve"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Ne ponavljaj nijednu"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Ponovi jednu"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne ponavljaj nijednu"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ponovi jednu"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ponovi sve"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Повтаряне на всички"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Без повтаряне"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Повтаряне на един елемент"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Без повтаряне"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Повтаряне на един елемент"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Повтаряне на всички"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Repeteix-ho tot"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"No en repeteixis cap"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repeteix-ne un"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No en repeteixis cap"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeteix una"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeteix tot"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Opakovat vše"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Neopakovat"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Opakovat jednu položku"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Neopakovat"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Opakovat jednu"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Opakovat vše"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Gentag alle"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Gentag ingen"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Gentag en"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Gentag ingen"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Gentag én"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Gentag alle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Alle wiederholen"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Keinen Titel wiederholen"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Einen Titel wiederholen"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Keinen wiederholen"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Einen wiederholen"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Alle wiederholen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Επανάληψη όλων"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Καμία επανάληψη"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Καμία επανάληψη"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Επανάληψη ενός κομματιού"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Επανάληψη όλων"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Repeat all"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Repeat all"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Repeat all"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Repetir todo"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No repetir"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repetir uno"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repetir todo"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Repetir todo"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No repetir"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repetir uno"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repetir todo"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"تکرار همه"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"تکرار هیچکدام"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"یکبار تکرار"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"تکرار هیچکدام"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"یکبار تکرار"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"تکرار همه"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Toista kaikki"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Toista ei mitään"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Toista yksi"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ei uudelleentoistoa"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Toista yksi uudelleen"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Toista kaikki uudelleen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Aucune répétition"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Répéter un élément"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne rien lire en boucle"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Lire une chanson en boucle"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Tout lire en boucle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Ne rien lire en boucle"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Lire en boucle un élément"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne rien lire en boucle"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Lire un titre en boucle"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Tout lire en boucle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"सभी को दोहराएं"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"कुछ भी न दोहराएं"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"एक दोहराएं"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"किसी को न दोहराएं"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"एक को दोहराएं"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"सभी को दोहराएं"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Ponovi sve"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Bez ponavljanja"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Ponovi jedno"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Bez ponavljanja"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ponovi jedno"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ponovi sve"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Összes ismétlése"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Nincs ismétlés"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Egy ismétlése"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Nincs ismétlés"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Egy szám ismétlése"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Összes szám ismétlése"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Ulangi Semua"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Jangan Ulangi"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Ulangi Satu"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Jangan ulangi"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ulangi 1"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ulangi semua"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Ripeti tutti"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Non ripetere nessuno"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Ripeti uno"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Non ripetere nulla"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ripeti uno"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ripeti tutto"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"חזור על הכל"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"אל תחזור על כלום"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"חזור על פריט אחד"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"אל תחזור על אף פריט"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"חזור על פריט אחד"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"חזור על הכול"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"全曲を繰り返し"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"繰り返しなし"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"1曲を繰り返し"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"リピートなし"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"1 曲をリピート"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"全曲をリピート"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"전체 반복"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"반복 안함"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"한 항목 반복"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"반복 안함"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"현재 미디어 반복"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"모두 반복"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Kartoti viską"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Nekartoti nieko"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Kartoti vieną"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Nekartoti nieko"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Kartoti vieną"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Kartoti viską"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Atkārtot visu"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Neatkārtot nevienu"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Atkārtot vienu"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Neatkārtot nevienu"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Atkārtot vienu"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Atkārtot visu"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Gjenta alle"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Ikke gjenta noen"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Gjenta én"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ikke gjenta noen"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Gjenta én"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Gjenta alle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -13,9 +12,11 @@
|
|||
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="exo_media_action_repeat_all_description">"Alles herhalen"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Niet herhalen"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Eén herhalen"</string>
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Niets herhalen"</string>
|
||||
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Eén herhalen"</string>
|
||||
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Alles herhalen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue