4
.gitignore
vendored
@ -43,6 +43,9 @@ cmake-build-debug
|
|||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
|
|
||||||
|
# External native builds
|
||||||
|
.externalNativeBuild
|
||||||
|
|
||||||
# VP9 extension
|
# VP9 extension
|
||||||
extensions/vp9/src/main/jni/libvpx
|
extensions/vp9/src/main/jni/libvpx
|
||||||
extensions/vp9/src/main/jni/libvpx_android_configs
|
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||||
@ -62,3 +65,4 @@ extensions/cronet/jniLibs/*
|
|||||||
!extensions/cronet/jniLibs/README.md
|
!extensions/cronet/jniLibs/README.md
|
||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
@ -38,4 +38,6 @@ devices and Android versions.
|
|||||||
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
|
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
|
||||||
log snippet is NOT sufficient. Please attach the captured bug report as a file.
|
log snippet is NOT sufficient. Please attach the captured bug report as a file.
|
||||||
If you don't wish to post it publicly, please submit the issue, then email the
|
If you don't wish to post it publicly, please submit the issue, then email the
|
||||||
bug report to dev.exoplayer@gmail.com using a subject in the format "Issue #1234".
|
bug report to dev.exoplayer@gmail.com using a subject in the format
|
||||||
|
"Issue #1234".
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ repositories {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Next add a gradle compile dependency to the `build.gradle` file of your app
|
Next add a dependency in the `build.gradle` file of your app module. The
|
||||||
module. The following will add a dependency to the full library:
|
following will add a dependency to the full library:
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||||
|
117
RELEASENOTES.md
@ -1,5 +1,119 @@
|
|||||||
# Release notes #
|
# Release notes #
|
||||||
|
|
||||||
|
### 2.8.0 ###
|
||||||
|
|
||||||
|
* Downloading:
|
||||||
|
* Add `DownloadService`, `DownloadManager` and related classes
|
||||||
|
([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on
|
||||||
|
using these components to download progressive formats can be found
|
||||||
|
[here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95).
|
||||||
|
To see how to download DASH, HLS and SmoothStreaming media, take a look at
|
||||||
|
the app.
|
||||||
|
* Updated main demo app to support downloading DASH, HLS, SmoothStreaming and
|
||||||
|
progressive media.
|
||||||
|
* MediaSources:
|
||||||
|
* Allow reusing media sources after they have been released and
|
||||||
|
also in parallel to allow adding them multiple times to a concatenation.
|
||||||
|
([#3498](https://github.com/google/ExoPlayer/issues/3498)).
|
||||||
|
* Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and
|
||||||
|
deprecated `DynamicConcatenatingMediaSource`.
|
||||||
|
* Allow clipping of child media sources where the period and window have a
|
||||||
|
non-zero offset with `ClippingMediaSource`.
|
||||||
|
* Allow adding and removing `MediaSourceEventListener`s to MediaSources after
|
||||||
|
they have been created. Listening to events is now supported for all
|
||||||
|
media sources including composite sources.
|
||||||
|
* Added callbacks to `MediaSourceEventListener` to get notified when media
|
||||||
|
periods are created, released and being read from.
|
||||||
|
* Support live stream clipping with `ClippingMediaSource`.
|
||||||
|
* Allow setting tags for all media sources in their factories. The tag of the
|
||||||
|
current window can be retrieved with `ExoPlayer.getCurrentTag`.
|
||||||
|
* UI components:
|
||||||
|
* Add support for displaying error messages and a buffering spinner in
|
||||||
|
`PlayerView`.
|
||||||
|
* Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
|
||||||
|
([#3736](https://github.com/google/ExoPlayer/issues/3736)).
|
||||||
|
* Add `PlayerNotificationManager` for displaying notifications reflecting the
|
||||||
|
player state.
|
||||||
|
* Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`.
|
||||||
|
* Add `TrackNameProvider` for converting track `Format`s to textual
|
||||||
|
descriptions, and `DefaultTrackNameProvider` as a default implementation.
|
||||||
|
* Track selection:
|
||||||
|
* Reworked `MappingTrackSelector` and `DefaultTrackSelector`.
|
||||||
|
* `DefaultTrackSelector.Parameters` now implements `Parcelable`.
|
||||||
|
* Added UI components for track selection (see above).
|
||||||
|
* Audio:
|
||||||
|
* Support extracting data from AMR container formats, including both narrow
|
||||||
|
and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)).
|
||||||
|
* FLAC:
|
||||||
|
* Sniff FLAC files correctly if they have ID3 headers
|
||||||
|
([#4055](https://github.com/google/ExoPlayer/issues/4055)).
|
||||||
|
* Supports FLAC files with high sample rate (176400 and 192000)
|
||||||
|
([#3769](https://github.com/google/ExoPlayer/issues/3769)).
|
||||||
|
* Factor out `AudioTrack` position tracking from `DefaultAudioSink`.
|
||||||
|
* Fix an issue where the playback position would pause just after playback
|
||||||
|
begins, and poll the audio timestamp less frequently once it starts
|
||||||
|
advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).
|
||||||
|
* Add an option to skip silent audio in `PlaybackParameters`
|
||||||
|
((#2635)[https://github.com/google/ExoPlayer/issues/2635]).
|
||||||
|
* Fix an issue where playback of TrueHD streams would get stuck after seeking
|
||||||
|
due to not finding a syncframe
|
||||||
|
((#3845)[https://github.com/google/ExoPlayer/issues/3845]).
|
||||||
|
* Fix an issue with eac3-joc playback where a codec would fail to configure
|
||||||
|
((#4165)[https://github.com/google/ExoPlayer/issues/4165]).
|
||||||
|
* Handle non-empty end-of-stream buffers, to fix gapless playback of streams
|
||||||
|
with encoder padding when the decoder returns a non-empty final buffer.
|
||||||
|
* Allow trimming more than one sample when applying an elst audio edit via
|
||||||
|
gapless playback info.
|
||||||
|
* Allow overriding skipping/scaling with custom `AudioProcessor`s
|
||||||
|
((#3142)[https://github.com/google/ExoPlayer/issues/3142]).
|
||||||
|
* Caching:
|
||||||
|
* Add release method to the `Cache` interface, and prevent multiple instances
|
||||||
|
of `SimpleCache` using the same folder at the same time.
|
||||||
|
* Cache redirect URLs
|
||||||
|
([#2360](https://github.com/google/ExoPlayer/issues/2360)).
|
||||||
|
* DRM:
|
||||||
|
* Allow multiple listeners for `DefaultDrmSessionManager`.
|
||||||
|
* Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.
|
||||||
|
* Change minimum API requirement for CBC and pattern encryption from 24 to 25
|
||||||
|
([#4022][https://github.com/google/ExoPlayer/issues/4022]).
|
||||||
|
* Fix handling of 307/308 redirects when making license requests
|
||||||
|
([#4108](https://github.com/google/ExoPlayer/issues/4108)).
|
||||||
|
* HLS:
|
||||||
|
* Fix playlist loading error propagation when the current selection does
|
||||||
|
not include all of the playlist's variants.
|
||||||
|
* Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods
|
||||||
|
([#4145](https://github.com/google/ExoPlayer/issues/4145)).
|
||||||
|
* Preeptively declare an ID3 track in chunkless preparation
|
||||||
|
([#4016](https://github.com/google/ExoPlayer/issues/4016)).
|
||||||
|
* Add support for multiple #EXT-X-MAP tags in a media playlist
|
||||||
|
([#4164](https://github.com/google/ExoPlayer/issues/4182)).
|
||||||
|
* Fix seeking in live streams
|
||||||
|
([#4187](https://github.com/google/ExoPlayer/issues/4187)).
|
||||||
|
* IMA:
|
||||||
|
* Allow setting the ad media load timeout
|
||||||
|
([#3691](https://github.com/google/ExoPlayer/issues/3691)).
|
||||||
|
* Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,
|
||||||
|
and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the
|
||||||
|
`AdsMediaSource.EventListener`.
|
||||||
|
* Add `AnalyticsListener` interface which can be registered in
|
||||||
|
`SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event.
|
||||||
|
* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within
|
||||||
|
a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
|
||||||
|
* Updated default max buffer length in `DefaultLoadControl`.
|
||||||
|
* Fix ClearKey decryption error if the key contains a forward slash
|
||||||
|
([#4075](https://github.com/google/ExoPlayer/issues/4075)).
|
||||||
|
* Fix crash when switching surface on Huawei P9 Lite
|
||||||
|
([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E
|
||||||
|
([#4104](https://github.com/google/ExoPlayer/issues/4104)).
|
||||||
|
* Support ZLIB compressed PGS subtitles.
|
||||||
|
* Added `getPlaybackError` to `Player` interface.
|
||||||
|
* Moved initial bitrate estimate from `AdaptiveTrackSelection` to
|
||||||
|
`DefaultBandwidthMeter`.
|
||||||
|
* Removed default renderer time offset of 60000000 from internal player. The
|
||||||
|
actual renderer timestamp offset can be obtained by listening to
|
||||||
|
`BaseRenderer.onStreamChanged`.
|
||||||
|
* Added dependencies on checkerframework annotations for static code analysis.
|
||||||
|
|
||||||
### 2.7.3 ###
|
### 2.7.3 ###
|
||||||
|
|
||||||
* Fix ProGuard configuration for Cast, IMA and OkHttp extensions.
|
* Fix ProGuard configuration for Cast, IMA and OkHttp extensions.
|
||||||
@ -93,7 +207,7 @@
|
|||||||
([#3630](https://github.com/google/ExoPlayer/issues/3630)).
|
([#3630](https://github.com/google/ExoPlayer/issues/3630)).
|
||||||
* DASH:
|
* DASH:
|
||||||
* Support in-band Emsg events targeting the player with scheme id
|
* Support in-band Emsg events targeting the player with scheme id
|
||||||
"urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3".
|
`urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3".
|
||||||
* Support EventStream elements in DASH manifests.
|
* Support EventStream elements in DASH manifests.
|
||||||
* HLS:
|
* HLS:
|
||||||
* Add opt-in support for chunkless preparation in HLS. This allows an
|
* Add opt-in support for chunkless preparation in HLS. This allows an
|
||||||
@ -163,6 +277,7 @@
|
|||||||
([#3792](https://github.com/google/ExoPlayer/issues/3792).
|
([#3792](https://github.com/google/ExoPlayer/issues/3792).
|
||||||
* Support 14-bit mode and little endianness in DTS PES packets
|
* Support 14-bit mode and little endianness in DTS PES packets
|
||||||
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
|
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
|
||||||
|
* Demo app: Add ability to download not DRM protected content.
|
||||||
|
|
||||||
### 2.6.1 ###
|
### 2.6.1 ###
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<?xml version="1.0"?>
|
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||||
<!--
|
|
||||||
Copyright (C) 2016 The Android Open Source Project
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -14,8 +12,8 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<lint>
|
||||||
<string name="exo_media_action_repeat_all_description">"Ulang semua"</string>
|
<issue id="InvalidPackage">
|
||||||
<string name="exo_media_action_repeat_off_description">"Tiada ulangan"</string>
|
<ignore path="**/checker-qual-*.jar"/>
|
||||||
<string name="exo_media_action_repeat_one_description">"Ulangan"</string>
|
</issue>
|
||||||
</resources>
|
</lint>
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.7.3'
|
releaseVersion = '2.8.0'
|
||||||
releaseVersionCode = 2703
|
releaseVersionCode = 2800
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||||
// components provided by the library may be of use on older devices.
|
// components provided by the library may be of use on older devices.
|
||||||
// However, please note that the core media playback functionality provided
|
// However, please note that the core media playback functionality provided
|
||||||
@ -25,12 +25,14 @@ project.ext {
|
|||||||
buildToolsVersion = '27.0.3'
|
buildToolsVersion = '27.0.3'
|
||||||
testSupportLibraryVersion = '0.5'
|
testSupportLibraryVersion = '0.5'
|
||||||
supportLibraryVersion = '27.0.0'
|
supportLibraryVersion = '27.0.0'
|
||||||
playServicesLibraryVersion = '11.4.2'
|
playServicesLibraryVersion = '12.0.0'
|
||||||
dexmakerVersion = '1.2'
|
dexmakerVersion = '1.2'
|
||||||
mockitoVersion = '1.9.5'
|
mockitoVersion = '1.9.5'
|
||||||
junitVersion = '4.12'
|
junitVersion = '4.12'
|
||||||
truthVersion = '0.39'
|
truthVersion = '0.39'
|
||||||
robolectricVersion = '3.7.1'
|
robolectricVersion = '3.7.1'
|
||||||
|
autoValueVersion = '1.6'
|
||||||
|
checkerframeworkVersion = '2.5.0'
|
||||||
modulePrefix = ':'
|
modulePrefix = ':'
|
||||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||||
|
@ -36,6 +36,7 @@ include modulePrefix + 'extension-opus'
|
|||||||
include modulePrefix + 'extension-vp9'
|
include modulePrefix + 'extension-vp9'
|
||||||
include modulePrefix + 'extension-rtmp'
|
include modulePrefix + 'extension-rtmp'
|
||||||
include modulePrefix + 'extension-leanback'
|
include modulePrefix + 'extension-leanback'
|
||||||
|
include modulePrefix + 'extension-jobdispatcher'
|
||||||
|
|
||||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
||||||
@ -56,6 +57,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi
|
|||||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||||
|
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
|
||||||
|
|
||||||
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
||||||
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
||||||
|
@ -32,7 +32,7 @@ import com.google.android.exoplayer2.Timeline;
|
|||||||
import com.google.android.exoplayer2.Timeline.Period;
|
import com.google.android.exoplayer2.Timeline.Period;
|
||||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
@ -80,8 +80,8 @@ import java.util.ArrayList;
|
|||||||
private final CastPlayer castPlayer;
|
private final CastPlayer castPlayer;
|
||||||
private final ArrayList<DemoUtil.Sample> mediaQueue;
|
private final ArrayList<DemoUtil.Sample> mediaQueue;
|
||||||
private final QueuePositionListener queuePositionListener;
|
private final QueuePositionListener queuePositionListener;
|
||||||
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
|
||||||
private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
|
|
||||||
private boolean castMediaQueueCreationPending;
|
private boolean castMediaQueueCreationPending;
|
||||||
private int currentItemIndex;
|
private int currentItemIndex;
|
||||||
private Player currentPlayer;
|
private Player currentPlayer;
|
||||||
@ -117,9 +117,10 @@ import java.util.ArrayList;
|
|||||||
this.castControlView = castControlView;
|
this.castControlView = castControlView;
|
||||||
mediaQueue = new ArrayList<>();
|
mediaQueue = new ArrayList<>();
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
|
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
|
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||||
exoPlayer.addListener(this);
|
exoPlayer.addListener(this);
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
localPlayerView.setPlayer(exoPlayer);
|
||||||
@ -155,9 +156,8 @@ import java.util.ArrayList;
|
|||||||
*/
|
*/
|
||||||
public void addItem(Sample sample) {
|
public void addItem(Sample sample) {
|
||||||
mediaQueue.add(sample);
|
mediaQueue.add(sample);
|
||||||
if (currentPlayer == exoPlayer) {
|
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
||||||
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
if (currentPlayer == castPlayer) {
|
||||||
} else {
|
|
||||||
castPlayer.addItems(buildMediaQueueItem(sample));
|
castPlayer.addItems(buildMediaQueueItem(sample));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,9 +186,8 @@ import java.util.ArrayList;
|
|||||||
* @return Whether the removal was successful.
|
* @return Whether the removal was successful.
|
||||||
*/
|
*/
|
||||||
public boolean removeItem(int itemIndex) {
|
public boolean removeItem(int itemIndex) {
|
||||||
if (currentPlayer == exoPlayer) {
|
concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||||
dynamicConcatenatingMediaSource.removeMediaSource(itemIndex);
|
if (currentPlayer == castPlayer) {
|
||||||
} else {
|
|
||||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||||
@ -215,9 +214,8 @@ import java.util.ArrayList;
|
|||||||
*/
|
*/
|
||||||
public boolean moveItem(int fromIndex, int toIndex) {
|
public boolean moveItem(int fromIndex, int toIndex) {
|
||||||
// Player update.
|
// Player update.
|
||||||
if (currentPlayer == exoPlayer) {
|
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||||
dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
} else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
int periodCount = castTimeline.getPeriodCount();
|
int periodCount = castTimeline.getPeriodCount();
|
||||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||||
@ -263,6 +261,7 @@ import java.util.ArrayList;
|
|||||||
public void release() {
|
public void release() {
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
mediaQueue.clear();
|
mediaQueue.clear();
|
||||||
|
concatenatingMediaSource.clear();
|
||||||
castPlayer.setSessionAvailabilityListener(null);
|
castPlayer.setSessionAvailabilityListener(null);
|
||||||
castPlayer.release();
|
castPlayer.release();
|
||||||
localPlayerView.setPlayer(null);
|
localPlayerView.setPlayer(null);
|
||||||
@ -354,11 +353,7 @@ import java.util.ArrayList;
|
|||||||
// Media queue management.
|
// Media queue management.
|
||||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
||||||
if (currentPlayer == exoPlayer) {
|
if (currentPlayer == exoPlayer) {
|
||||||
dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource();
|
exoPlayer.prepare(concatenatingMediaSource);
|
||||||
for (int i = 0; i < mediaQueue.size(); i++) {
|
|
||||||
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
|
|
||||||
}
|
|
||||||
exoPlayer.prepare(dynamicConcatenatingMediaSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playback transition.
|
// Playback transition.
|
||||||
|
@ -17,8 +17,6 @@ package com.google.android.exoplayer2.imademo;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.C.ContentType;
|
import com.google.android.exoplayer2.C.ContentType;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
@ -27,7 +25,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
|||||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||||
@ -83,8 +80,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
|
|
||||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||||
String contentUrl = context.getString(R.string.content_url);
|
String contentUrl = context.getString(R.string.content_url);
|
||||||
MediaSource contentMediaSource =
|
MediaSource contentMediaSource = buildMediaSource(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.
|
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||||
MediaSource mediaSourceWithAds =
|
MediaSource mediaSourceWithAds =
|
||||||
@ -121,9 +117,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
// AdsMediaSource.MediaSourceFactory implementation.
|
// AdsMediaSource.MediaSourceFactory implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaSource createMediaSource(
|
public MediaSource createMediaSource(Uri uri) {
|
||||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
return buildMediaSource(uri);
|
||||||
return buildMediaSource(uri, handler, listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -134,25 +129,22 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private MediaSource buildMediaSource(
|
private MediaSource buildMediaSource(Uri uri) {
|
||||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
|
||||||
@ContentType int type = Util.inferContentType(uri);
|
@ContentType int type = Util.inferContentType(uri);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(
|
||||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||||
manifestDataSourceFactory)
|
manifestDataSourceFactory)
|
||||||
.createMediaSource(uri, handler, listener);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(
|
return new SsMediaSource.Factory(
|
||||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
||||||
.createMediaSource(uri, handler, listener);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||||
.createMediaSource(uri, handler, listener);
|
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||||
.createMediaSource(uri, handler, listener);
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||||
<uses-sdk/>
|
<uses-sdk/>
|
||||||
@ -73,6 +75,18 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="true"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -578,5 +578,16 @@
|
|||||||
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
|
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ABR",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "Random ABR - Google Glass (MP4,H264)",
|
||||||
|
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
||||||
|
"extension": "mpd",
|
||||||
|
"abr_algorithm": "random"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -16,20 +16,51 @@
|
|||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||||
|
import com.google.android.exoplayer2.offline.ProgressiveDownloadAction;
|
||||||
|
import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction;
|
||||||
|
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||||
*/
|
*/
|
||||||
public class DemoApplication extends Application {
|
public class DemoApplication extends Application {
|
||||||
|
|
||||||
|
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||||
|
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||||
|
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||||
|
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
||||||
|
private static final Deserializer[] DOWNLOAD_DESERIALIZERS =
|
||||||
|
new Deserializer[] {
|
||||||
|
DashDownloadAction.DESERIALIZER,
|
||||||
|
HlsDownloadAction.DESERIALIZER,
|
||||||
|
SsDownloadAction.DESERIALIZER,
|
||||||
|
ProgressiveDownloadAction.DESERIALIZER
|
||||||
|
};
|
||||||
|
|
||||||
protected String userAgent;
|
protected String userAgent;
|
||||||
|
|
||||||
|
private File downloadDirectory;
|
||||||
|
private Cache downloadCache;
|
||||||
|
private DownloadManager downloadManager;
|
||||||
|
private DownloadTracker downloadTracker;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
@ -38,7 +69,9 @@ public class DemoApplication extends Application {
|
|||||||
|
|
||||||
/** Returns a {@link DataSource.Factory}. */
|
/** Returns a {@link DataSource.Factory}. */
|
||||||
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
|
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
|
||||||
return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
|
DefaultDataSourceFactory upstreamFactory =
|
||||||
|
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
|
||||||
|
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a {@link HttpDataSource.Factory}. */
|
/** Returns a {@link HttpDataSource.Factory}. */
|
||||||
@ -47,8 +80,69 @@ public class DemoApplication extends Application {
|
|||||||
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether extension renderers should be used. */
|
||||||
public boolean useExtensionRenderers() {
|
public boolean useExtensionRenderers() {
|
||||||
return BuildConfig.FLAVOR.equals("withExtensions");
|
return "withExtensions".equals(BuildConfig.FLAVOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DownloadManager getDownloadManager() {
|
||||||
|
initDownloadManager();
|
||||||
|
return downloadManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadTracker getDownloadTracker() {
|
||||||
|
initDownloadManager();
|
||||||
|
return downloadTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void initDownloadManager() {
|
||||||
|
if (downloadManager == null) {
|
||||||
|
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||||
|
new DownloaderConstructorHelper(
|
||||||
|
getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null));
|
||||||
|
downloadManager =
|
||||||
|
new DownloadManager(
|
||||||
|
downloaderConstructorHelper,
|
||||||
|
MAX_SIMULTANEOUS_DOWNLOADS,
|
||||||
|
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
||||||
|
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
||||||
|
DOWNLOAD_DESERIALIZERS);
|
||||||
|
downloadTracker =
|
||||||
|
new DownloadTracker(
|
||||||
|
/* context= */ this,
|
||||||
|
buildDataSourceFactory(/* listener= */ null),
|
||||||
|
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE),
|
||||||
|
DOWNLOAD_DESERIALIZERS);
|
||||||
|
downloadManager.addListener(downloadTracker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized Cache getDownloadCache() {
|
||||||
|
if (downloadCache == null) {
|
||||||
|
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
||||||
|
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
|
||||||
|
}
|
||||||
|
return downloadCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getDownloadDirectory() {
|
||||||
|
if (downloadDirectory == null) {
|
||||||
|
downloadDirectory = getExternalFilesDir(null);
|
||||||
|
if (downloadDirectory == null) {
|
||||||
|
downloadDirectory = getFilesDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return downloadDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
||||||
|
DefaultDataSourceFactory upstreamFactory, Cache cache) {
|
||||||
|
return new CacheDataSourceFactory(
|
||||||
|
cache,
|
||||||
|
upstreamFactory,
|
||||||
|
new FileDataSourceFactory(),
|
||||||
|
/* cacheWriteDataSinkFactory= */ null,
|
||||||
|
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
|
||||||
|
/* eventListener= */ null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.demo;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
|
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
||||||
|
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
|
||||||
|
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
/** A service for downloading media. */
|
||||||
|
public class DemoDownloadService extends DownloadService {
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "download_channel";
|
||||||
|
private static final int JOB_ID = 1;
|
||||||
|
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
public DemoDownloadService() {
|
||||||
|
super(
|
||||||
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
|
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||||
|
CHANNEL_ID,
|
||||||
|
R.string.exo_download_notification_channel_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DownloadManager getDownloadManager() {
|
||||||
|
return ((DemoApplication) getApplication()).getDownloadManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PlatformScheduler getScheduler() {
|
||||||
|
return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Notification getForegroundNotification(TaskState[] taskStates) {
|
||||||
|
return DownloadNotificationUtil.buildProgressNotification(
|
||||||
|
/* context= */ this,
|
||||||
|
R.drawable.exo_controls_play,
|
||||||
|
CHANNEL_ID,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
/* message= */ null,
|
||||||
|
taskStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTaskStateChanged(TaskState taskState) {
|
||||||
|
if (taskState.action.isRemoveAction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Notification notification = null;
|
||||||
|
if (taskState.state == TaskState.STATE_COMPLETED) {
|
||||||
|
notification =
|
||||||
|
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
||||||
|
/* context= */ this,
|
||||||
|
R.drawable.exo_controls_play,
|
||||||
|
CHANNEL_ID,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(taskState.action.data));
|
||||||
|
} else if (taskState.state == TaskState.STATE_FAILED) {
|
||||||
|
notification =
|
||||||
|
DownloadNotificationUtil.buildDownloadFailedNotification(
|
||||||
|
/* context= */ this,
|
||||||
|
R.drawable.exo_controls_play,
|
||||||
|
CHANNEL_ID,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(taskState.action.data));
|
||||||
|
}
|
||||||
|
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
|
||||||
|
NotificationUtil.setNotification(this, notificationId, notification);
|
||||||
|
}
|
||||||
|
}
|
@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.demo;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility methods for demo application.
|
|
||||||
*/
|
|
||||||
/* package */ final class DemoUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a track name for display.
|
|
||||||
*
|
|
||||||
* @param format {@link Format} of the track.
|
|
||||||
* @return a generated name specific to the track.
|
|
||||||
*/
|
|
||||||
public static String buildTrackName(Format format) {
|
|
||||||
String trackName;
|
|
||||||
if (MimeTypes.isVideo(format.sampleMimeType)) {
|
|
||||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
|
|
||||||
buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
|
|
||||||
buildSampleMimeTypeString(format));
|
|
||||||
} else if (MimeTypes.isAudio(format.sampleMimeType)) {
|
|
||||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
|
|
||||||
buildLanguageString(format), buildAudioPropertyString(format)),
|
|
||||||
buildBitrateString(format)), buildTrackIdString(format)),
|
|
||||||
buildSampleMimeTypeString(format));
|
|
||||||
} else {
|
|
||||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
|
|
||||||
buildBitrateString(format)), buildTrackIdString(format)),
|
|
||||||
buildSampleMimeTypeString(format));
|
|
||||||
}
|
|
||||||
return trackName.length() == 0 ? "unknown" : trackName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildResolutionString(Format format) {
|
|
||||||
return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE
|
|
||||||
? "" : format.width + "x" + format.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildAudioPropertyString(Format format) {
|
|
||||||
return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE
|
|
||||||
? "" : format.channelCount + "ch, " + format.sampleRate + "Hz";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildLanguageString(Format format) {
|
|
||||||
return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? ""
|
|
||||||
: format.language;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildBitrateString(Format format) {
|
|
||||||
return format.bitrate == Format.NO_VALUE ? ""
|
|
||||||
: String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String joinWithSeparator(String first, String second) {
|
|
||||||
return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildTrackIdString(Format format) {
|
|
||||||
return format.id == null ? "" : ("id:" + format.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildSampleMimeTypeString(Format format) {
|
|
||||||
return format.sampleMimeType == null ? "" : format.sampleMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DemoUtil() {}
|
|
||||||
}
|
|
@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
* 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.demo;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.offline.ActionFile;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
|
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
||||||
|
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
|
||||||
|
import com.google.android.exoplayer2.offline.TrackKey;
|
||||||
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
|
||||||
|
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
|
||||||
|
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
|
||||||
|
import com.google.android.exoplayer2.ui.TrackNameProvider;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks media that has been downloaded.
|
||||||
|
*
|
||||||
|
* <p>Tracked downloads are persisted using an {@link ActionFile}, however in a real application
|
||||||
|
* it's expected that state will be stored directly in the application's media database, so that it
|
||||||
|
* can be queried efficiently together with other information about the media.
|
||||||
|
*/
|
||||||
|
public class DownloadTracker implements DownloadManager.Listener {
|
||||||
|
|
||||||
|
/** Listens for changes in the tracked downloads. */
|
||||||
|
public interface Listener {
|
||||||
|
|
||||||
|
/** Called when the tracked downloads changed. */
|
||||||
|
void onDownloadsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "DownloadTracker";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
private final TrackNameProvider trackNameProvider;
|
||||||
|
private final CopyOnWriteArraySet<Listener> listeners;
|
||||||
|
private final HashMap<Uri, DownloadAction> trackedDownloadStates;
|
||||||
|
private final ActionFile actionFile;
|
||||||
|
private final Handler actionFileWriteHandler;
|
||||||
|
|
||||||
|
public DownloadTracker(
|
||||||
|
Context context,
|
||||||
|
DataSource.Factory dataSourceFactory,
|
||||||
|
File actionFile,
|
||||||
|
DownloadAction.Deserializer[] deserializers) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
|
this.actionFile = new ActionFile(actionFile);
|
||||||
|
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
|
||||||
|
listeners = new CopyOnWriteArraySet<>();
|
||||||
|
trackedDownloadStates = new HashMap<>();
|
||||||
|
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
||||||
|
actionFileWriteThread.start();
|
||||||
|
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
||||||
|
loadTrackedActions(deserializers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Listener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Listener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDownloaded(Uri uri) {
|
||||||
|
return trackedDownloadStates.containsKey(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <K> List<K> getOfflineStreamKeys(Uri uri) {
|
||||||
|
if (!trackedDownloadStates.containsKey(uri)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
DownloadAction action = trackedDownloadStates.get(uri);
|
||||||
|
if (action instanceof SegmentDownloadAction) {
|
||||||
|
return ((SegmentDownloadAction) action).keys;
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
|
||||||
|
if (isDownloaded(uri)) {
|
||||||
|
DownloadAction removeAction =
|
||||||
|
getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name));
|
||||||
|
startServiceWithAction(removeAction);
|
||||||
|
} else {
|
||||||
|
StartDownloadDialogHelper helper =
|
||||||
|
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
|
||||||
|
helper.prepare();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadManager.Listener
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitialized(DownloadManager downloadManager) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
|
||||||
|
DownloadAction action = taskState.action;
|
||||||
|
Uri uri = action.uri;
|
||||||
|
if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
|
||||||
|
|| (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
|
||||||
|
// A download has been removed, or has failed. Stop tracking it.
|
||||||
|
if (trackedDownloadStates.remove(uri) != null) {
|
||||||
|
handleTrackedDownloadStatesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onIdle(DownloadManager downloadManager) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods
|
||||||
|
|
||||||
|
private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) {
|
||||||
|
try {
|
||||||
|
DownloadAction[] allActions = actionFile.load(deserializers);
|
||||||
|
for (DownloadAction action : allActions) {
|
||||||
|
trackedDownloadStates.put(action.uri, action);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to load tracked actions", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTrackedDownloadStatesChanged() {
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onDownloadsChanged();
|
||||||
|
}
|
||||||
|
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
|
||||||
|
actionFileWriteHandler.post(
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
actionFile.store(actions);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to store tracked actions", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDownload(DownloadAction action) {
|
||||||
|
if (trackedDownloadStates.containsKey(action.uri)) {
|
||||||
|
// This content is already being downloaded. Do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trackedDownloadStates.put(action.uri, action);
|
||||||
|
handleTrackedDownloadStatesChanged();
|
||||||
|
startServiceWithAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startServiceWithAction(DownloadAction action) {
|
||||||
|
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadHelper getDownloadHelper(Uri uri, String extension) {
|
||||||
|
int type = Util.inferContentType(uri, extension);
|
||||||
|
switch (type) {
|
||||||
|
case C.TYPE_DASH:
|
||||||
|
return new DashDownloadHelper(uri, dataSourceFactory);
|
||||||
|
case C.TYPE_SS:
|
||||||
|
return new SsDownloadHelper(uri, dataSourceFactory);
|
||||||
|
case C.TYPE_HLS:
|
||||||
|
return new HlsDownloadHelper(uri, dataSourceFactory);
|
||||||
|
case C.TYPE_OTHER:
|
||||||
|
return new ProgressiveDownloadHelper(uri);
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StartDownloadDialogHelper
|
||||||
|
implements DownloadHelper.Callback, DialogInterface.OnClickListener {
|
||||||
|
|
||||||
|
private final DownloadHelper downloadHelper;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final AlertDialog.Builder builder;
|
||||||
|
private final View dialogView;
|
||||||
|
private final List<TrackKey> trackKeys;
|
||||||
|
private final ArrayAdapter<String> trackTitles;
|
||||||
|
private final ListView representationList;
|
||||||
|
|
||||||
|
public StartDownloadDialogHelper(
|
||||||
|
Activity activity, DownloadHelper downloadHelper, String name) {
|
||||||
|
this.downloadHelper = downloadHelper;
|
||||||
|
this.name = name;
|
||||||
|
builder =
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.exo_download_description)
|
||||||
|
.setPositiveButton(android.R.string.ok, this)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
|
||||||
|
// Inflate with the builder's context to ensure the correct style is used.
|
||||||
|
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
|
||||||
|
dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
|
||||||
|
|
||||||
|
trackKeys = new ArrayList<>();
|
||||||
|
trackTitles =
|
||||||
|
new ArrayAdapter<>(
|
||||||
|
builder.getContext(), android.R.layout.simple_list_item_multiple_choice);
|
||||||
|
representationList = dialogView.findViewById(R.id.representation_list);
|
||||||
|
representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||||
|
representationList.setAdapter(trackTitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepare() {
|
||||||
|
downloadHelper.prepare(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepared(DownloadHelper helper) {
|
||||||
|
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
|
||||||
|
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
|
||||||
|
for (int j = 0; j < trackGroups.length; j++) {
|
||||||
|
TrackGroup trackGroup = trackGroups.get(j);
|
||||||
|
for (int k = 0; k < trackGroup.length; k++) {
|
||||||
|
trackKeys.add(new TrackKey(i, j, k));
|
||||||
|
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!trackKeys.isEmpty()) {
|
||||||
|
builder.setView(dialogView);
|
||||||
|
}
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareError(DownloadHelper helper, IOException e) {
|
||||||
|
Toast.makeText(
|
||||||
|
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>();
|
||||||
|
for (int i = 0; i < representationList.getChildCount(); i++) {
|
||||||
|
if (representationList.isItemChecked(i)) {
|
||||||
|
selectedTrackKeys.add(trackKeys.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) {
|
||||||
|
// We have selected keys, or we're dealing with single stream content.
|
||||||
|
DownloadAction downloadAction =
|
||||||
|
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
|
||||||
|
startDownload(downloadAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,14 +16,14 @@
|
|||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.util.Pair;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
@ -42,43 +42,52 @@ import com.google.android.exoplayer2.PlaybackPreparer;
|
|||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||||
|
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.RenditionKey;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
|
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||||
import com.google.android.exoplayer2.util.EventLogger;
|
import com.google.android.exoplayer2.util.EventLogger;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||||
@ -86,10 +95,10 @@ public class PlayerActivity extends Activity
|
|||||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||||
|
|
||||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||||
public static final String DRM_LICENSE_URL = "drm_license_url";
|
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||||
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
|
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||||
public static final String DRM_MULTI_SESSION = "drm_multi_session";
|
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||||
public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
|
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||||
|
|
||||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||||
public static final String EXTENSION_EXTRA = "extension";
|
public static final String EXTENSION_EXTRA = "extension";
|
||||||
@ -98,11 +107,22 @@ public class PlayerActivity extends Activity
|
|||||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||||
public static final String URI_LIST_EXTRA = "uri_list";
|
public static final String URI_LIST_EXTRA = "uri_list";
|
||||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
||||||
|
|
||||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||||
|
|
||||||
// For backwards compatibility.
|
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||||
|
private static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||||
|
private static final String ABR_ALGORITHM_RANDOM = "random";
|
||||||
|
|
||||||
|
// For backwards compatibility only.
|
||||||
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||||
|
|
||||||
|
// Saved instance state keys.
|
||||||
|
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
||||||
|
private static final String KEY_WINDOW = "window";
|
||||||
|
private static final String KEY_POSITION = "position";
|
||||||
|
private static final String KEY_AUTO_PLAY = "auto_play";
|
||||||
|
|
||||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||||
static {
|
static {
|
||||||
@ -110,23 +130,21 @@ public class PlayerActivity extends Activity
|
|||||||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Handler mainHandler;
|
|
||||||
private EventLogger eventLogger;
|
|
||||||
private PlayerView playerView;
|
private PlayerView playerView;
|
||||||
private LinearLayout debugRootView;
|
private LinearLayout debugRootView;
|
||||||
private TextView debugTextView;
|
private TextView debugTextView;
|
||||||
|
|
||||||
private DataSource.Factory mediaDataSourceFactory;
|
private DataSource.Factory mediaDataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
|
private MediaSource mediaSource;
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private TrackSelectionHelper trackSelectionHelper;
|
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
private DebugTextViewHelper debugViewHelper;
|
private DebugTextViewHelper debugViewHelper;
|
||||||
private boolean inErrorState;
|
|
||||||
private TrackGroupArray lastSeenTrackGroupArray;
|
private TrackGroupArray lastSeenTrackGroupArray;
|
||||||
|
|
||||||
private boolean shouldAutoPlay;
|
private boolean startAutoPlay;
|
||||||
private int resumeWindow;
|
private int startWindow;
|
||||||
private long resumePosition;
|
private long startPosition;
|
||||||
|
|
||||||
// Fields used only for ad playback. The ads loader is loaded via reflection.
|
// Fields used only for ad playback. The ads loader is loaded via reflection.
|
||||||
|
|
||||||
@ -139,10 +157,7 @@ public class PlayerActivity extends Activity
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
shouldAutoPlay = true;
|
|
||||||
clearResumePosition();
|
|
||||||
mediaDataSourceFactory = buildDataSourceFactory(true);
|
mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||||
mainHandler = new Handler();
|
|
||||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||||
}
|
}
|
||||||
@ -155,14 +170,24 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
playerView = findViewById(R.id.player_view);
|
playerView = findViewById(R.id.player_view);
|
||||||
playerView.setControllerVisibilityListener(this);
|
playerView.setControllerVisibilityListener(this);
|
||||||
|
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||||
playerView.requestFocus();
|
playerView.requestFocus();
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
||||||
|
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
|
||||||
|
startWindow = savedInstanceState.getInt(KEY_WINDOW);
|
||||||
|
startPosition = savedInstanceState.getLong(KEY_POSITION);
|
||||||
|
} else {
|
||||||
|
trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
|
||||||
|
clearStartPosition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(Intent intent) {
|
public void onNewIntent(Intent intent) {
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
shouldAutoPlay = true;
|
clearStartPosition();
|
||||||
clearResumePosition();
|
|
||||||
setIntent(intent);
|
setIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +232,12 @@ public class PlayerActivity extends Activity
|
|||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
@NonNull int[] grantResults) {
|
@NonNull int[] grantResults) {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length == 0) {
|
||||||
|
// Empty results are triggered if a permission is requested while another request was already
|
||||||
|
// pending and can be safely ignored in this case.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
} else {
|
} else {
|
||||||
showToast(R.string.storage_permission_denied);
|
showToast(R.string.storage_permission_denied);
|
||||||
@ -215,6 +245,16 @@ public class PlayerActivity extends Activity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
updateTrackSelectorParameters();
|
||||||
|
updateStartPosition();
|
||||||
|
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
|
||||||
|
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
|
||||||
|
outState.putInt(KEY_WINDOW, startWindow);
|
||||||
|
outState.putLong(KEY_POSITION, startPosition);
|
||||||
|
}
|
||||||
|
|
||||||
// Activity input
|
// Activity input
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -230,8 +270,19 @@ public class PlayerActivity extends Activity
|
|||||||
if (view.getParent() == debugRootView) {
|
if (view.getParent() == debugRootView) {
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||||
if (mappedTrackInfo != null) {
|
if (mappedTrackInfo != null) {
|
||||||
trackSelectionHelper.showSelectionDialog(
|
CharSequence title = ((Button) view).getText();
|
||||||
this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag());
|
int rendererIndex = (int) view.getTag();
|
||||||
|
int rendererType = mappedTrackInfo.getRendererType(rendererIndex);
|
||||||
|
boolean allowAdaptiveSelections =
|
||||||
|
rendererType == C.TRACK_TYPE_VIDEO
|
||||||
|
|| (rendererType == C.TRACK_TYPE_AUDIO
|
||||||
|
&& mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||||
|
== MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS);
|
||||||
|
Pair<AlertDialog, TrackSelectionView> dialogPair =
|
||||||
|
TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex);
|
||||||
|
dialogPair.second.setShowDisableOption(true);
|
||||||
|
dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
||||||
|
dialogPair.first.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,21 +304,40 @@ public class PlayerActivity extends Activity
|
|||||||
// Internal methods
|
// Internal methods
|
||||||
|
|
||||||
private void initializePlayer() {
|
private void initializePlayer() {
|
||||||
|
if (player == null) {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
boolean needNewPlayer = player == null;
|
String action = intent.getAction();
|
||||||
if (needNewPlayer) {
|
Uri[] uris;
|
||||||
TrackSelection.Factory adaptiveTrackSelectionFactory =
|
String[] extensions;
|
||||||
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
if (ACTION_VIEW.equals(action)) {
|
||||||
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
|
uris = new Uri[] {intent.getData()};
|
||||||
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
|
extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
|
||||||
lastSeenTrackGroupArray = null;
|
} else if (ACTION_VIEW_LIST.equals(action)) {
|
||||||
eventLogger = new EventLogger(trackSelector);
|
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
|
||||||
|
uris = new Uri[uriStrings.length];
|
||||||
|
for (int i = 0; i < uriStrings.length; i++) {
|
||||||
|
uris[i] = Uri.parse(uriStrings[i]);
|
||||||
|
}
|
||||||
|
extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
|
||||||
|
if (extensions == null) {
|
||||||
|
extensions = new String[uriStrings.length];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(getString(R.string.unexpected_intent_action, action));
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
|
||||||
|
// The player will be reinitialized if the permission is granted.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||||
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
|
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
|
||||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
|
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
|
||||||
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
|
String[] keyRequestPropertiesArray =
|
||||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
|
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);
|
||||||
|
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);
|
||||||
int errorStringId = R.string.error_drm_unknown;
|
int errorStringId = R.string.error_drm_unknown;
|
||||||
if (Util.SDK_INT < 18) {
|
if (Util.SDK_INT < 18) {
|
||||||
errorStringId = R.string.error_drm_not_supported;
|
errorStringId = R.string.error_drm_not_supported;
|
||||||
@ -290,62 +360,53 @@ public class PlayerActivity extends Activity
|
|||||||
}
|
}
|
||||||
if (drmSessionManager == null) {
|
if (drmSessionManager == null) {
|
||||||
showToast(errorStringId);
|
showToast(errorStringId);
|
||||||
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
|
TrackSelection.Factory trackSelectionFactory;
|
||||||
|
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||||
|
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||||
|
trackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
||||||
|
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
||||||
|
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||||
|
} else {
|
||||||
|
showToast(R.string.error_unrecognized_abr_algorithm);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean preferExtensionDecoders =
|
||||||
|
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
||||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
((DemoApplication) getApplication()).useExtensionRenderers()
|
||||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
|
DefaultRenderersFactory renderersFactory =
|
||||||
drmSessionManager, extensionRendererMode);
|
new DefaultRenderersFactory(this, extensionRendererMode);
|
||||||
|
|
||||||
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||||
|
trackSelector.setParameters(trackSelectorParameters);
|
||||||
|
lastSeenTrackGroupArray = null;
|
||||||
|
|
||||||
|
player =
|
||||||
|
ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager);
|
||||||
player.addListener(new PlayerEventListener());
|
player.addListener(new PlayerEventListener());
|
||||||
player.addListener(eventLogger);
|
player.setPlayWhenReady(startAutoPlay);
|
||||||
player.addMetadataOutput(eventLogger);
|
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||||
player.addAudioDebugListener(eventLogger);
|
|
||||||
player.addVideoDebugListener(eventLogger);
|
|
||||||
player.setPlayWhenReady(shouldAutoPlay);
|
|
||||||
|
|
||||||
playerView.setPlayer(player);
|
playerView.setPlayer(player);
|
||||||
playerView.setPlaybackPreparer(this);
|
playerView.setPlaybackPreparer(this);
|
||||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||||
debugViewHelper.start();
|
debugViewHelper.start();
|
||||||
}
|
|
||||||
String action = intent.getAction();
|
|
||||||
Uri[] uris;
|
|
||||||
String[] extensions;
|
|
||||||
if (ACTION_VIEW.equals(action)) {
|
|
||||||
uris = new Uri[]{intent.getData()};
|
|
||||||
extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)};
|
|
||||||
} else if (ACTION_VIEW_LIST.equals(action)) {
|
|
||||||
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
|
|
||||||
uris = new Uri[uriStrings.length];
|
|
||||||
for (int i = 0; i < uriStrings.length; i++) {
|
|
||||||
uris[i] = Uri.parse(uriStrings[i]);
|
|
||||||
}
|
|
||||||
extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
|
|
||||||
if (extensions == null) {
|
|
||||||
extensions = new String[uriStrings.length];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast(getString(R.string.unexpected_intent_action, action));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
|
|
||||||
// The player will be reinitialized if the permission is granted.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MediaSource[] mediaSources = new MediaSource[uris.length];
|
MediaSource[] mediaSources = new MediaSource[uris.length];
|
||||||
for (int i = 0; i < uris.length; i++) {
|
for (int i = 0; i < uris.length; i++) {
|
||||||
mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger);
|
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
|
||||||
}
|
}
|
||||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
mediaSource =
|
||||||
: new ConcatenatingMediaSource(mediaSources);
|
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||||
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
||||||
if (adTagUriString != null) {
|
if (adTagUriString != null) {
|
||||||
Uri adTagUri = Uri.parse(adTagUriString);
|
Uri adTagUri = Uri.parse(adTagUriString);
|
||||||
@ -362,82 +423,105 @@ public class PlayerActivity extends Activity
|
|||||||
} else {
|
} else {
|
||||||
releaseAdsLoader();
|
releaseAdsLoader();
|
||||||
}
|
}
|
||||||
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
|
||||||
if (haveResumePosition) {
|
|
||||||
player.seekTo(resumeWindow, resumePosition);
|
|
||||||
}
|
}
|
||||||
player.prepare(mediaSource, !haveResumePosition, false);
|
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||||
inErrorState = false;
|
if (haveStartPosition) {
|
||||||
|
player.seekTo(startWindow, startPosition);
|
||||||
|
}
|
||||||
|
player.prepare(mediaSource, !haveStartPosition, false);
|
||||||
updateButtonVisibilities();
|
updateButtonVisibilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource buildMediaSource(
|
private MediaSource buildMediaSource(Uri uri) {
|
||||||
Uri uri,
|
return buildMediaSource(uri, null);
|
||||||
String overrideExtension,
|
}
|
||||||
@Nullable Handler handler,
|
|
||||||
@Nullable MediaSourceEventListener listener) {
|
@SuppressWarnings("unchecked")
|
||||||
@ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
|
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||||
: Util.inferContentType("." + overrideExtension);
|
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(
|
||||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||||
buildDataSourceFactory(false))
|
buildDataSourceFactory(false))
|
||||||
.createMediaSource(uri, handler, listener);
|
.setManifestParser(
|
||||||
|
new FilteringManifestParser<>(
|
||||||
|
new DashManifestParser(), (List<RepresentationKey>) getOfflineStreamKeys(uri)))
|
||||||
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(
|
return new SsMediaSource.Factory(
|
||||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
||||||
buildDataSourceFactory(false))
|
buildDataSourceFactory(false))
|
||||||
.createMediaSource(uri, handler, listener);
|
.setManifestParser(
|
||||||
|
new FilteringManifestParser<>(
|
||||||
|
new SsManifestParser(), (List<StreamKey>) getOfflineStreamKeys(uri)))
|
||||||
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||||
.createMediaSource(uri, handler, listener);
|
.setPlaylistParser(
|
||||||
|
new FilteringManifestParser<>(
|
||||||
|
new HlsPlaylistParser(), (List<RenditionKey>) getOfflineStreamKeys(uri)))
|
||||||
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||||
.createMediaSource(uri, handler, listener);
|
|
||||||
default: {
|
default: {
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(UUID uuid,
|
private List<?> getOfflineStreamKeys(Uri uri) {
|
||||||
String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
||||||
|
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||||
throws UnsupportedDrmException {
|
throws UnsupportedDrmException {
|
||||||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
|
HttpDataSource.Factory licenseDataSourceFactory =
|
||||||
buildHttpDataSourceFactory(false));
|
((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
|
||||||
|
HttpMediaDrmCallback drmCallback =
|
||||||
|
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
||||||
if (keyRequestPropertiesArray != null) {
|
if (keyRequestPropertiesArray != null) {
|
||||||
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
||||||
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
|
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
|
||||||
keyRequestPropertiesArray[i + 1]);
|
keyRequestPropertiesArray[i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
|
return new DefaultDrmSessionManager<>(
|
||||||
null, mainHandler, eventLogger, multiSession);
|
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releasePlayer() {
|
private void releasePlayer() {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
|
updateTrackSelectorParameters();
|
||||||
|
updateStartPosition();
|
||||||
debugViewHelper.stop();
|
debugViewHelper.stop();
|
||||||
debugViewHelper = null;
|
debugViewHelper = null;
|
||||||
shouldAutoPlay = player.getPlayWhenReady();
|
|
||||||
updateResumePosition();
|
|
||||||
player.release();
|
player.release();
|
||||||
player = null;
|
player = null;
|
||||||
|
mediaSource = null;
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
trackSelectionHelper = null;
|
|
||||||
eventLogger = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateResumePosition() {
|
private void updateTrackSelectorParameters() {
|
||||||
resumeWindow = player.getCurrentWindowIndex();
|
if (trackSelector != null) {
|
||||||
resumePosition = Math.max(0, player.getContentPosition());
|
trackSelectorParameters = trackSelector.getParameters();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearResumePosition() {
|
private void updateStartPosition() {
|
||||||
resumeWindow = C.INDEX_UNSET;
|
if (player != null) {
|
||||||
resumePosition = C.TIME_UNSET;
|
startAutoPlay = player.getPlayWhenReady();
|
||||||
|
startWindow = player.getCurrentWindowIndex();
|
||||||
|
startPosition = Math.max(0, player.getContentPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearStartPosition() {
|
||||||
|
startAutoPlay = true;
|
||||||
|
startWindow = C.INDEX_UNSET;
|
||||||
|
startPosition = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -452,18 +536,6 @@ public class PlayerActivity extends Activity
|
|||||||
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
|
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new HttpDataSource factory.
|
|
||||||
*
|
|
||||||
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
|
|
||||||
* DataSource factory.
|
|
||||||
* @return A new HttpDataSource factory.
|
|
||||||
*/
|
|
||||||
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
|
|
||||||
return ((DemoApplication) getApplication())
|
|
||||||
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||||
private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
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.
|
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
||||||
@ -486,10 +558,8 @@ public class PlayerActivity extends Activity
|
|||||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||||
new AdsMediaSource.MediaSourceFactory() {
|
new AdsMediaSource.MediaSourceFactory() {
|
||||||
@Override
|
@Override
|
||||||
public MediaSource createMediaSource(
|
public MediaSource createMediaSource(Uri uri) {
|
||||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
return PlayerActivity.this.buildMediaSource(uri);
|
||||||
return PlayerActivity.this.buildMediaSource(
|
|
||||||
uri, /* overrideExtension= */ null, handler, listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -497,8 +567,7 @@ public class PlayerActivity extends Activity
|
|||||||
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return new AdsMediaSource(
|
return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup);
|
||||||
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
|
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
// IMA extension not loaded.
|
// IMA extension not loaded.
|
||||||
return null;
|
return null;
|
||||||
@ -529,20 +598,20 @@ public class PlayerActivity extends Activity
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < mappedTrackInfo.length; i++) {
|
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
|
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
|
||||||
if (trackGroups.length != 0) {
|
if (trackGroups.length != 0) {
|
||||||
Button button = new Button(this);
|
Button button = new Button(this);
|
||||||
int label;
|
int label;
|
||||||
switch (player.getRendererType(i)) {
|
switch (player.getRendererType(i)) {
|
||||||
case C.TRACK_TYPE_AUDIO:
|
case C.TRACK_TYPE_AUDIO:
|
||||||
label = R.string.audio;
|
label = R.string.exo_track_selection_title_audio;
|
||||||
break;
|
break;
|
||||||
case C.TRACK_TYPE_VIDEO:
|
case C.TRACK_TYPE_VIDEO:
|
||||||
label = R.string.video;
|
label = R.string.exo_track_selection_title_video;
|
||||||
break;
|
break;
|
||||||
case C.TRACK_TYPE_TEXT:
|
case C.TRACK_TYPE_TEXT:
|
||||||
label = R.string.text;
|
label = R.string.exo_track_selection_title_text;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
@ -593,48 +662,20 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||||
if (inErrorState) {
|
if (player.getPlaybackError() != null) {
|
||||||
// This will only occur if the user has performed a seek whilst in the error state. Update
|
// The user has performed a seek whilst in the error state. Update the resume position so
|
||||||
// the resume position so that if the user then retries, playback will resume from the
|
// that if the user then retries, playback resumes from the position to which they seeked.
|
||||||
// position to which they seeked.
|
updateStartPosition();
|
||||||
updateResumePosition();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(ExoPlaybackException e) {
|
public void onPlayerError(ExoPlaybackException e) {
|
||||||
String errorString = null;
|
|
||||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
|
||||||
Exception cause = e.getRendererException();
|
|
||||||
if (cause instanceof DecoderInitializationException) {
|
|
||||||
// Special case for decoder initialization failures.
|
|
||||||
DecoderInitializationException decoderInitializationException =
|
|
||||||
(DecoderInitializationException) cause;
|
|
||||||
if (decoderInitializationException.decoderName == null) {
|
|
||||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
|
||||||
errorString = getString(R.string.error_querying_decoders);
|
|
||||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
|
||||||
errorString = getString(R.string.error_no_secure_decoder,
|
|
||||||
decoderInitializationException.mimeType);
|
|
||||||
} else {
|
|
||||||
errorString = getString(R.string.error_no_decoder,
|
|
||||||
decoderInitializationException.mimeType);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorString = getString(R.string.error_instantiating_decoder,
|
|
||||||
decoderInitializationException.decoderName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errorString != null) {
|
|
||||||
showToast(errorString);
|
|
||||||
}
|
|
||||||
inErrorState = true;
|
|
||||||
if (isBehindLiveWindow(e)) {
|
if (isBehindLiveWindow(e)) {
|
||||||
clearResumePosition();
|
clearStartPosition();
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
} else {
|
} else {
|
||||||
updateResumePosition();
|
updateStartPosition();
|
||||||
updateButtonVisibilities();
|
updateButtonVisibilities();
|
||||||
showControls();
|
showControls();
|
||||||
}
|
}
|
||||||
@ -647,11 +688,11 @@ public class PlayerActivity extends Activity
|
|||||||
if (trackGroups != lastSeenTrackGroupArray) {
|
if (trackGroups != lastSeenTrackGroupArray) {
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||||
if (mappedTrackInfo != null) {
|
if (mappedTrackInfo != null) {
|
||||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
|
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||||
showToast(R.string.error_unsupported_video);
|
showToast(R.string.error_unsupported_video);
|
||||||
}
|
}
|
||||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
|
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||||
showToast(R.string.error_unsupported_audio);
|
showToast(R.string.error_unsupported_audio);
|
||||||
}
|
}
|
||||||
@ -659,7 +700,40 @@ public class PlayerActivity extends Activity
|
|||||||
lastSeenTrackGroupArray = trackGroups;
|
lastSeenTrackGroupArray = trackGroups;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
|
||||||
|
String errorString = getString(R.string.error_generic);
|
||||||
|
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||||
|
Exception cause = e.getRendererException();
|
||||||
|
if (cause instanceof DecoderInitializationException) {
|
||||||
|
// Special case for decoder initialization failures.
|
||||||
|
DecoderInitializationException decoderInitializationException =
|
||||||
|
(DecoderInitializationException) cause;
|
||||||
|
if (decoderInitializationException.decoderName == null) {
|
||||||
|
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||||
|
errorString = getString(R.string.error_querying_decoders);
|
||||||
|
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||||
|
errorString =
|
||||||
|
getString(
|
||||||
|
R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
|
||||||
|
} else {
|
||||||
|
errorString =
|
||||||
|
getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorString =
|
||||||
|
getString(
|
||||||
|
R.string.error_instantiating_decoder,
|
||||||
|
decoderInitializationException.decoderName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair.create(0, errorString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,15 +24,17 @@ import android.os.AsyncTask;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.JsonReader;
|
import android.util.JsonReader;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.BaseExpandableListAdapter;
|
import android.widget.BaseExpandableListAdapter;
|
||||||
import android.widget.ExpandableListView;
|
import android.widget.ExpandableListView;
|
||||||
import android.widget.ExpandableListView.OnChildClickListener;
|
import android.widget.ExpandableListView.OnChildClickListener;
|
||||||
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
@ -44,20 +46,27 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/** An activity for selecting from a list of media samples. */
|
||||||
* An activity for selecting from a list of samples.
|
public class SampleChooserActivity extends Activity
|
||||||
*/
|
implements DownloadTracker.Listener, OnChildClickListener {
|
||||||
public class SampleChooserActivity extends Activity {
|
|
||||||
|
|
||||||
private static final String TAG = "SampleChooserActivity";
|
private static final String TAG = "SampleChooserActivity";
|
||||||
|
|
||||||
|
private DownloadTracker downloadTracker;
|
||||||
|
private SampleAdapter sampleAdapter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.sample_chooser_activity);
|
setContentView(R.layout.sample_chooser_activity);
|
||||||
|
sampleAdapter = new SampleAdapter();
|
||||||
|
ExpandableListView sampleListView = findViewById(R.id.sample_list);
|
||||||
|
sampleListView.setAdapter(sampleAdapter);
|
||||||
|
sampleListView.setOnChildClickListener(this);
|
||||||
|
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
String dataUri = intent.getDataString();
|
String dataUri = intent.getDataString();
|
||||||
String[] uris;
|
String[] uris;
|
||||||
@ -80,8 +89,32 @@ public class SampleChooserActivity extends Activity {
|
|||||||
uriList.toArray(uris);
|
uriList.toArray(uris);
|
||||||
Arrays.sort(uris);
|
Arrays.sort(uris);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker();
|
||||||
SampleListLoader loaderTask = new SampleListLoader();
|
SampleListLoader loaderTask = new SampleListLoader();
|
||||||
loaderTask.execute(uris);
|
loaderTask.execute(uris);
|
||||||
|
|
||||||
|
// Ping the download service in case it's not running (but should be).
|
||||||
|
startService(
|
||||||
|
new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
downloadTracker.addListener(this);
|
||||||
|
sampleAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
downloadTracker.removeListener(this);
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownloadsChanged() {
|
||||||
|
sampleAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
|
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
|
||||||
@ -89,20 +122,44 @@ public class SampleChooserActivity extends Activity {
|
|||||||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
ExpandableListView sampleList = findViewById(R.id.sample_list);
|
sampleAdapter.setSampleGroups(groups);
|
||||||
sampleList.setAdapter(new SampleAdapter(this, groups));
|
|
||||||
sampleList.setOnChildClickListener(new OnChildClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
|
|
||||||
int childPosition, long id) {
|
|
||||||
onSampleSelected(groups.get(groupPosition).samples.get(childPosition));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleSelected(Sample sample) {
|
@Override
|
||||||
|
public boolean onChildClick(
|
||||||
|
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
||||||
|
Sample sample = (Sample) view.getTag();
|
||||||
startActivity(sample.buildIntent(this));
|
startActivity(sample.buildIntent(this));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSampleDownloadButtonClicked(Sample sample) {
|
||||||
|
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
|
||||||
|
if (downloadUnsupportedStringId != 0) {
|
||||||
|
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
UriSample uriSample = (UriSample) sample;
|
||||||
|
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getDownloadUnsupportedStringId(Sample sample) {
|
||||||
|
if (sample instanceof PlaylistSample) {
|
||||||
|
return R.string.download_playlist_unsupported;
|
||||||
|
}
|
||||||
|
UriSample uriSample = (UriSample) sample;
|
||||||
|
if (uriSample.drmInfo != null) {
|
||||||
|
return R.string.download_drm_unsupported;
|
||||||
|
}
|
||||||
|
if (uriSample.adTagUri != null) {
|
||||||
|
return R.string.download_ads_unsupported;
|
||||||
|
}
|
||||||
|
String scheme = uriSample.uri.getScheme();
|
||||||
|
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
||||||
|
return R.string.download_scheme_unsupported;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
|
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
|
||||||
@ -176,15 +233,16 @@ public class SampleChooserActivity extends Activity {
|
|||||||
|
|
||||||
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||||
String sampleName = null;
|
String sampleName = null;
|
||||||
String uri = null;
|
Uri uri = null;
|
||||||
String extension = null;
|
String extension = null;
|
||||||
UUID drmUuid = null;
|
String drmScheme = null;
|
||||||
String drmLicenseUrl = null;
|
String drmLicenseUrl = null;
|
||||||
String[] drmKeyRequestProperties = null;
|
String[] drmKeyRequestProperties = null;
|
||||||
boolean drmMultiSession = false;
|
boolean drmMultiSession = false;
|
||||||
boolean preferExtensionDecoders = false;
|
boolean preferExtensionDecoders = false;
|
||||||
ArrayList<UriSample> playlistSamples = null;
|
ArrayList<UriSample> playlistSamples = null;
|
||||||
String adTagUri = null;
|
String adTagUri = null;
|
||||||
|
String abrAlgorithm = null;
|
||||||
|
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
@ -194,16 +252,14 @@ public class SampleChooserActivity extends Activity {
|
|||||||
sampleName = reader.nextString();
|
sampleName = reader.nextString();
|
||||||
break;
|
break;
|
||||||
case "uri":
|
case "uri":
|
||||||
uri = reader.nextString();
|
uri = Uri.parse(reader.nextString());
|
||||||
break;
|
break;
|
||||||
case "extension":
|
case "extension":
|
||||||
extension = reader.nextString();
|
extension = reader.nextString();
|
||||||
break;
|
break;
|
||||||
case "drm_scheme":
|
case "drm_scheme":
|
||||||
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
||||||
String drmScheme = reader.nextString();
|
drmScheme = reader.nextString();
|
||||||
drmUuid = Util.getDrmUuid(drmScheme);
|
|
||||||
Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme);
|
|
||||||
break;
|
break;
|
||||||
case "drm_license_url":
|
case "drm_license_url":
|
||||||
Assertions.checkState(!insidePlaylist,
|
Assertions.checkState(!insidePlaylist,
|
||||||
@ -242,21 +298,28 @@ public class SampleChooserActivity extends Activity {
|
|||||||
case "ad_tag_uri":
|
case "ad_tag_uri":
|
||||||
adTagUri = reader.nextString();
|
adTagUri = reader.nextString();
|
||||||
break;
|
break;
|
||||||
|
case "abr_algorithm":
|
||||||
|
Assertions.checkState(
|
||||||
|
!insidePlaylist, "Invalid attribute on nested item: abr_algorithm");
|
||||||
|
abrAlgorithm = reader.nextString();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ParserException("Unsupported attribute name: " + name);
|
throw new ParserException("Unsupported attribute name: " + name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl,
|
DrmInfo drmInfo =
|
||||||
drmKeyRequestProperties, drmMultiSession);
|
drmScheme == null
|
||||||
|
? null
|
||||||
|
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
|
||||||
if (playlistSamples != null) {
|
if (playlistSamples != null) {
|
||||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
||||||
new UriSample[playlistSamples.size()]);
|
new UriSample[playlistSamples.size()]);
|
||||||
return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo,
|
return new PlaylistSample(
|
||||||
playlistSamplesArray);
|
sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray);
|
||||||
} else {
|
} else {
|
||||||
return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension,
|
return new UriSample(
|
||||||
adTagUri);
|
sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, uri, extension, adTagUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,14 +336,17 @@ public class SampleChooserActivity extends Activity {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SampleAdapter extends BaseExpandableListAdapter {
|
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
||||||
|
|
||||||
private final Context context;
|
private List<SampleGroup> sampleGroups;
|
||||||
private final List<SampleGroup> sampleGroups;
|
|
||||||
|
|
||||||
public SampleAdapter(Context context, List<SampleGroup> sampleGroups) {
|
public SampleAdapter() {
|
||||||
this.context = context;
|
sampleGroups = Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSampleGroups(List<SampleGroup> sampleGroups) {
|
||||||
this.sampleGroups = sampleGroups;
|
this.sampleGroups = sampleGroups;
|
||||||
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -298,10 +364,12 @@ public class SampleChooserActivity extends Activity {
|
|||||||
View convertView, ViewGroup parent) {
|
View convertView, ViewGroup parent) {
|
||||||
View view = convertView;
|
View view = convertView;
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent,
|
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
|
||||||
false);
|
View downloadButton = view.findViewById(R.id.download_button);
|
||||||
|
downloadButton.setOnClickListener(this);
|
||||||
|
downloadButton.setFocusable(false);
|
||||||
}
|
}
|
||||||
((TextView) view).setText(getChild(groupPosition, childPosition).name);
|
initializeChildView(view, getChild(groupPosition, childPosition));
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,8 +393,9 @@ public class SampleChooserActivity extends Activity {
|
|||||||
ViewGroup parent) {
|
ViewGroup parent) {
|
||||||
View view = convertView;
|
View view = convertView;
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1,
|
view =
|
||||||
parent, false);
|
getLayoutInflater()
|
||||||
|
.inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
|
||||||
}
|
}
|
||||||
((TextView) view).setText(getGroup(groupPosition).title);
|
((TextView) view).setText(getGroup(groupPosition).title);
|
||||||
return view;
|
return view;
|
||||||
@ -347,6 +416,25 @@ public class SampleChooserActivity extends Activity {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
onSampleDownloadButtonClicked((Sample) view.getTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeChildView(View view, Sample sample) {
|
||||||
|
view.setTag(sample);
|
||||||
|
TextView sampleTitle = view.findViewById(R.id.sample_title);
|
||||||
|
sampleTitle.setText(sample.name);
|
||||||
|
|
||||||
|
boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
|
||||||
|
boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
|
||||||
|
ImageButton downloadButton = view.findViewById(R.id.download_button);
|
||||||
|
downloadButton.setTag(sample);
|
||||||
|
downloadButton.setColorFilter(
|
||||||
|
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE);
|
||||||
|
downloadButton.setImageResource(
|
||||||
|
isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SampleGroup {
|
private static final class SampleGroup {
|
||||||
@ -362,14 +450,17 @@ public class SampleChooserActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class DrmInfo {
|
private static final class DrmInfo {
|
||||||
public final UUID drmSchemeUuid;
|
public final String drmScheme;
|
||||||
public final String drmLicenseUrl;
|
public final String drmLicenseUrl;
|
||||||
public final String[] drmKeyRequestProperties;
|
public final String[] drmKeyRequestProperties;
|
||||||
public final boolean drmMultiSession;
|
public final boolean drmMultiSession;
|
||||||
|
|
||||||
public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl,
|
public DrmInfo(
|
||||||
String[] drmKeyRequestProperties, boolean drmMultiSession) {
|
String drmScheme,
|
||||||
this.drmSchemeUuid = drmSchemeUuid;
|
String drmLicenseUrl,
|
||||||
|
String[] drmKeyRequestProperties,
|
||||||
|
boolean drmMultiSession) {
|
||||||
|
this.drmScheme = drmScheme;
|
||||||
this.drmLicenseUrl = drmLicenseUrl;
|
this.drmLicenseUrl = drmLicenseUrl;
|
||||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||||
this.drmMultiSession = drmMultiSession;
|
this.drmMultiSession = drmMultiSession;
|
||||||
@ -377,31 +468,34 @@ public class SampleChooserActivity extends Activity {
|
|||||||
|
|
||||||
public void updateIntent(Intent intent) {
|
public void updateIntent(Intent intent) {
|
||||||
Assertions.checkNotNull(intent);
|
Assertions.checkNotNull(intent);
|
||||||
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString());
|
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);
|
||||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
|
intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);
|
||||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
|
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);
|
||||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
|
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract static class Sample {
|
private abstract static class Sample {
|
||||||
public final String name;
|
public final String name;
|
||||||
public final boolean preferExtensionDecoders;
|
public final boolean preferExtensionDecoders;
|
||||||
|
public final String abrAlgorithm;
|
||||||
public final DrmInfo drmInfo;
|
public final DrmInfo drmInfo;
|
||||||
|
|
||||||
public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) {
|
public Sample(
|
||||||
|
String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.preferExtensionDecoders = preferExtensionDecoders;
|
this.preferExtensionDecoders = preferExtensionDecoders;
|
||||||
|
this.abrAlgorithm = abrAlgorithm;
|
||||||
this.drmInfo = drmInfo;
|
this.drmInfo = drmInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Intent buildIntent(Context context) {
|
public Intent buildIntent(Context context) {
|
||||||
Intent intent = new Intent(context, PlayerActivity.class);
|
Intent intent = new Intent(context, PlayerActivity.class);
|
||||||
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
|
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
|
||||||
|
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||||
if (drmInfo != null) {
|
if (drmInfo != null) {
|
||||||
drmInfo.updateIntent(intent);
|
drmInfo.updateIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,13 +503,19 @@ public class SampleChooserActivity extends Activity {
|
|||||||
|
|
||||||
private static final class UriSample extends Sample {
|
private static final class UriSample extends Sample {
|
||||||
|
|
||||||
public final String uri;
|
public final Uri uri;
|
||||||
public final String extension;
|
public final String extension;
|
||||||
public final String adTagUri;
|
public final String adTagUri;
|
||||||
|
|
||||||
public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri,
|
public UriSample(
|
||||||
String extension, String adTagUri) {
|
String name,
|
||||||
super(name, preferExtensionDecoders, drmInfo);
|
boolean preferExtensionDecoders,
|
||||||
|
String abrAlgorithm,
|
||||||
|
DrmInfo drmInfo,
|
||||||
|
Uri uri,
|
||||||
|
String extension,
|
||||||
|
String adTagUri) {
|
||||||
|
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
this.adTagUri = adTagUri;
|
this.adTagUri = adTagUri;
|
||||||
@ -424,7 +524,7 @@ public class SampleChooserActivity extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public Intent buildIntent(Context context) {
|
public Intent buildIntent(Context context) {
|
||||||
return super.buildIntent(context)
|
return super.buildIntent(context)
|
||||||
.setData(Uri.parse(uri))
|
.setData(uri)
|
||||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||||
.setAction(PlayerActivity.ACTION_VIEW);
|
.setAction(PlayerActivity.ACTION_VIEW);
|
||||||
@ -436,9 +536,13 @@ public class SampleChooserActivity extends Activity {
|
|||||||
|
|
||||||
public final UriSample[] children;
|
public final UriSample[] children;
|
||||||
|
|
||||||
public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo,
|
public PlaylistSample(
|
||||||
|
String name,
|
||||||
|
boolean preferExtensionDecoders,
|
||||||
|
String abrAlgorithm,
|
||||||
|
DrmInfo drmInfo,
|
||||||
UriSample... children) {
|
UriSample... children) {
|
||||||
super(name, preferExtensionDecoders, drmInfo);
|
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
||||||
this.children = children;
|
this.children = children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,7 +551,7 @@ public class SampleChooserActivity extends Activity {
|
|||||||
String[] uris = new String[children.length];
|
String[] uris = new String[children.length];
|
||||||
String[] extensions = new String[children.length];
|
String[] extensions = new String[children.length];
|
||||||
for (int i = 0; i < children.length; i++) {
|
for (int i = 0; i < children.length; i++) {
|
||||||
uris[i] = children[i].uri;
|
uris[i] = children[i].uri.toString();
|
||||||
extensions[i] = children[i].extension;
|
extensions[i] = children[i].extension;
|
||||||
}
|
}
|
||||||
return super.buildIntent(context)
|
return super.buildIntent(context)
|
||||||
|
@ -1,290 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
* 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.demo;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckedTextView;
|
|
||||||
import com.google.android.exoplayer2.RendererCapabilities;
|
|
||||||
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.MappingTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride;
|
|
||||||
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for displaying track selection dialogs.
|
|
||||||
*/
|
|
||||||
/* package */ final class TrackSelectionHelper implements View.OnClickListener,
|
|
||||||
DialogInterface.OnClickListener {
|
|
||||||
|
|
||||||
private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory();
|
|
||||||
private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
|
|
||||||
|
|
||||||
private final MappingTrackSelector selector;
|
|
||||||
private final TrackSelection.Factory adaptiveTrackSelectionFactory;
|
|
||||||
|
|
||||||
private MappedTrackInfo trackInfo;
|
|
||||||
private int rendererIndex;
|
|
||||||
private TrackGroupArray trackGroups;
|
|
||||||
private boolean[] trackGroupsAdaptive;
|
|
||||||
private boolean isDisabled;
|
|
||||||
private SelectionOverride override;
|
|
||||||
|
|
||||||
private CheckedTextView disableView;
|
|
||||||
private CheckedTextView defaultView;
|
|
||||||
private CheckedTextView enableRandomAdaptationView;
|
|
||||||
private CheckedTextView[][] trackViews;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param selector The track selector.
|
|
||||||
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
|
|
||||||
* if the selection helper should not support adaptive tracks.
|
|
||||||
*/
|
|
||||||
public TrackSelectionHelper(MappingTrackSelector selector,
|
|
||||||
TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
|
||||||
this.selector = selector;
|
|
||||||
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the selection dialog for a given renderer.
|
|
||||||
*
|
|
||||||
* @param activity The parent activity.
|
|
||||||
* @param title The dialog's title.
|
|
||||||
* @param trackInfo The current track information.
|
|
||||||
* @param rendererIndex The index of the renderer.
|
|
||||||
*/
|
|
||||||
public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo,
|
|
||||||
int rendererIndex) {
|
|
||||||
this.trackInfo = trackInfo;
|
|
||||||
this.rendererIndex = rendererIndex;
|
|
||||||
|
|
||||||
trackGroups = trackInfo.getTrackGroups(rendererIndex);
|
|
||||||
trackGroupsAdaptive = new boolean[trackGroups.length];
|
|
||||||
for (int i = 0; i < trackGroups.length; i++) {
|
|
||||||
trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
|
|
||||||
&& trackInfo.getAdaptiveSupport(rendererIndex, i, false)
|
|
||||||
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
|
|
||||||
&& trackGroups.get(i).length > 1;
|
|
||||||
}
|
|
||||||
isDisabled = selector.getRendererDisabled(rendererIndex);
|
|
||||||
override = selector.getSelectionOverride(rendererIndex, trackGroups);
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
||||||
builder.setTitle(title)
|
|
||||||
.setView(buildView(builder.getContext()))
|
|
||||||
.setPositiveButton(android.R.string.ok, this)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private View buildView(Context context) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(context);
|
|
||||||
View view = inflater.inflate(R.layout.track_selection_dialog, null);
|
|
||||||
ViewGroup root = view.findViewById(R.id.root);
|
|
||||||
|
|
||||||
TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
|
|
||||||
new int[] {android.R.attr.selectableItemBackground});
|
|
||||||
int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
|
|
||||||
attributeArray.recycle();
|
|
||||||
|
|
||||||
// View for disabling the renderer.
|
|
||||||
disableView = (CheckedTextView) inflater.inflate(
|
|
||||||
android.R.layout.simple_list_item_single_choice, root, false);
|
|
||||||
disableView.setBackgroundResource(selectableItemBackgroundResourceId);
|
|
||||||
disableView.setText(R.string.selection_disabled);
|
|
||||||
disableView.setFocusable(true);
|
|
||||||
disableView.setOnClickListener(this);
|
|
||||||
root.addView(disableView);
|
|
||||||
|
|
||||||
// View for clearing the override to allow the selector to use its default selection logic.
|
|
||||||
defaultView = (CheckedTextView) inflater.inflate(
|
|
||||||
android.R.layout.simple_list_item_single_choice, root, false);
|
|
||||||
defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
|
|
||||||
defaultView.setText(R.string.selection_default);
|
|
||||||
defaultView.setFocusable(true);
|
|
||||||
defaultView.setOnClickListener(this);
|
|
||||||
root.addView(inflater.inflate(R.layout.list_divider, root, false));
|
|
||||||
root.addView(defaultView);
|
|
||||||
|
|
||||||
// Per-track views.
|
|
||||||
boolean haveAdaptiveTracks = false;
|
|
||||||
trackViews = new CheckedTextView[trackGroups.length][];
|
|
||||||
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
|
|
||||||
TrackGroup group = trackGroups.get(groupIndex);
|
|
||||||
boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex];
|
|
||||||
haveAdaptiveTracks |= groupIsAdaptive;
|
|
||||||
trackViews[groupIndex] = new CheckedTextView[group.length];
|
|
||||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
|
||||||
if (trackIndex == 0) {
|
|
||||||
root.addView(inflater.inflate(R.layout.list_divider, root, false));
|
|
||||||
}
|
|
||||||
int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice
|
|
||||||
: android.R.layout.simple_list_item_single_choice;
|
|
||||||
CheckedTextView trackView = (CheckedTextView) inflater.inflate(
|
|
||||||
trackViewLayoutId, root, false);
|
|
||||||
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
|
|
||||||
trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex)));
|
|
||||||
if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)
|
|
||||||
== RendererCapabilities.FORMAT_HANDLED) {
|
|
||||||
trackView.setFocusable(true);
|
|
||||||
trackView.setTag(Pair.create(groupIndex, trackIndex));
|
|
||||||
trackView.setOnClickListener(this);
|
|
||||||
} else {
|
|
||||||
trackView.setFocusable(false);
|
|
||||||
trackView.setEnabled(false);
|
|
||||||
}
|
|
||||||
trackViews[groupIndex][trackIndex] = trackView;
|
|
||||||
root.addView(trackView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (haveAdaptiveTracks) {
|
|
||||||
// View for using random adaptation.
|
|
||||||
enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
|
|
||||||
android.R.layout.simple_list_item_multiple_choice, root, false);
|
|
||||||
enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId);
|
|
||||||
enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
|
|
||||||
enableRandomAdaptationView.setOnClickListener(this);
|
|
||||||
root.addView(inflater.inflate(R.layout.list_divider, root, false));
|
|
||||||
root.addView(enableRandomAdaptationView);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateViews();
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateViews() {
|
|
||||||
disableView.setChecked(isDisabled);
|
|
||||||
defaultView.setChecked(!isDisabled && override == null);
|
|
||||||
for (int i = 0; i < trackViews.length; i++) {
|
|
||||||
for (int j = 0; j < trackViews[i].length; j++) {
|
|
||||||
trackViews[i][j].setChecked(override != null && override.groupIndex == i
|
|
||||||
&& override.containsTrack(j));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (enableRandomAdaptationView != null) {
|
|
||||||
boolean enableView = !isDisabled && override != null && override.length > 1;
|
|
||||||
enableRandomAdaptationView.setEnabled(enableView);
|
|
||||||
enableRandomAdaptationView.setFocusable(enableView);
|
|
||||||
if (enableView) {
|
|
||||||
enableRandomAdaptationView.setChecked(!isDisabled
|
|
||||||
&& override.factory instanceof RandomTrackSelection.Factory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialogInterface.OnClickListener
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
selector.setRendererDisabled(rendererIndex, isDisabled);
|
|
||||||
if (override != null) {
|
|
||||||
selector.setSelectionOverride(rendererIndex, trackGroups, override);
|
|
||||||
} else {
|
|
||||||
selector.clearSelectionOverrides(rendererIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View.OnClickListener
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (view == disableView) {
|
|
||||||
isDisabled = true;
|
|
||||||
override = null;
|
|
||||||
} else if (view == defaultView) {
|
|
||||||
isDisabled = false;
|
|
||||||
override = null;
|
|
||||||
} else if (view == enableRandomAdaptationView) {
|
|
||||||
setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked());
|
|
||||||
} else {
|
|
||||||
isDisabled = false;
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Pair<Integer, Integer> tag = (Pair<Integer, Integer>) view.getTag();
|
|
||||||
int groupIndex = tag.first;
|
|
||||||
int trackIndex = tag.second;
|
|
||||||
if (!trackGroupsAdaptive[groupIndex] || override == null
|
|
||||||
|| override.groupIndex != groupIndex) {
|
|
||||||
override = new SelectionOverride(FIXED_FACTORY, groupIndex, trackIndex);
|
|
||||||
} else {
|
|
||||||
// The group being modified is adaptive and we already have a non-null override.
|
|
||||||
boolean isEnabled = ((CheckedTextView) view).isChecked();
|
|
||||||
int overrideLength = override.length;
|
|
||||||
if (isEnabled) {
|
|
||||||
// Remove the track from the override.
|
|
||||||
if (overrideLength == 1) {
|
|
||||||
// The last track is being removed, so the override becomes empty.
|
|
||||||
override = null;
|
|
||||||
isDisabled = true;
|
|
||||||
} else {
|
|
||||||
setOverride(groupIndex, getTracksRemoving(override, trackIndex),
|
|
||||||
enableRandomAdaptationView.isChecked());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Add the track to the override.
|
|
||||||
setOverride(groupIndex, getTracksAdding(override, trackIndex),
|
|
||||||
enableRandomAdaptationView.isChecked());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update the views with the new state.
|
|
||||||
updateViews();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
|
|
||||||
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
|
|
||||||
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
|
|
||||||
override = new SelectionOverride(factory, group, tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track array manipulation.
|
|
||||||
|
|
||||||
private static int[] getTracksAdding(SelectionOverride override, int addedTrack) {
|
|
||||||
int[] tracks = override.tracks;
|
|
||||||
tracks = Arrays.copyOf(tracks, tracks.length + 1);
|
|
||||||
tracks[tracks.length - 1] = addedTrack;
|
|
||||||
return tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) {
|
|
||||||
int[] tracks = new int[override.length - 1];
|
|
||||||
int trackCount = 0;
|
|
||||||
for (int i = 0; i < tracks.length + 1; i++) {
|
|
||||||
int track = override.tracks[i];
|
|
||||||
if (track != removedTrack) {
|
|
||||||
tracks[trackCount++] = track;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
# 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);
|
|
||||||
}
|
|
BIN
demos/main/src/main/res/drawable-hdpi/ic_download.png
Normal file
After Width: | Height: | Size: 199 B |
BIN
demos/main/src/main/res/drawable-hdpi/ic_download_done.png
Normal file
After Width: | Height: | Size: 218 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_download.png
Normal file
After Width: | Height: | Size: 163 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_download_done.png
Normal file
After Width: | Height: | Size: 182 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_download.png
Normal file
After Width: | Height: | Size: 187 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_download_done.png
Normal file
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_download.png
Normal file
After Width: | Height: | Size: 303 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png
Normal file
After Width: | Height: | Size: 450 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_download.png
Normal file
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png
Normal file
After Width: | Height: | Size: 575 B |
38
demos/main/src/main/res/layout/sample_list_item.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView android:id="@+id/sample_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/download_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/exo_download_description"
|
||||||
|
android:background="@android:color/transparent"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||||
Copyright (C) 2016 The Android Open Source Project
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -14,8 +13,7 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
|
android:id="@+id/representation_list"
|
||||||
<string name="exo_media_action_repeat_off_description">"Non repetir"</string>
|
android:layout_width="match_parent"
|
||||||
<string name="exo_media_action_repeat_one_description">"Repetir un"</string>
|
android:layout_height="match_parent"/>
|
||||||
</resources>
|
|
@ -17,19 +17,11 @@
|
|||||||
|
|
||||||
<string name="application_name">ExoPlayer</string>
|
<string name="application_name">ExoPlayer</string>
|
||||||
|
|
||||||
<string name="video">Video</string>
|
|
||||||
|
|
||||||
<string name="audio">Audio</string>
|
|
||||||
|
|
||||||
<string name="text">Text</string>
|
|
||||||
|
|
||||||
<string name="selection_disabled">Disabled</string>
|
|
||||||
|
|
||||||
<string name="selection_default">Default</string>
|
|
||||||
|
|
||||||
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
||||||
|
|
||||||
<string name="enable_random_adaptation">Enable random adaptation</string>
|
<string name="error_generic">Playback failed</string>
|
||||||
|
|
||||||
|
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
||||||
|
|
||||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||||
|
|
||||||
@ -55,4 +47,14 @@
|
|||||||
|
|
||||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||||
|
|
||||||
|
<string name="download_start_error">Failed to start download</string>
|
||||||
|
|
||||||
|
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||||
|
|
||||||
|
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
|
||||||
|
|
||||||
|
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
|
||||||
|
|
||||||
|
<string name="download_ads_unsupported">IMA does not support offline ads</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -30,9 +30,9 @@ dependencies {
|
|||||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
||||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
||||||
// used, for example:
|
// used, for example:
|
||||||
// com.google.android.gms:play-services-cast-framework:11.4.2
|
// com.google.android.gms:play-services-cast-framework:12.0.0
|
||||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
// |-- com.google.android.gms:play-services-basement:12.0.0
|
||||||
// |-- com.android.support:support-v4:25.2.0
|
// |-- com.android.support:support-v4:26.1.0
|
||||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||||
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||||
|
@ -19,6 +19,7 @@ import android.support.annotation.NonNull;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
@ -307,6 +308,11 @@ public final class CastPlayer implements Player {
|
|||||||
return playbackState;
|
return playbackState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExoPlaybackException getPlaybackError() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setPlayWhenReady(boolean playWhenReady) {
|
public void setPlayWhenReady(boolean playWhenReady) {
|
||||||
if (remoteMediaClient == null) {
|
if (remoteMediaClient == null) {
|
||||||
@ -481,6 +487,14 @@ public final class CastPlayer implements Player {
|
|||||||
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getCurrentTag() {
|
||||||
|
int windowIndex = getCurrentWindowIndex();
|
||||||
|
return windowIndex > currentTimeline.getWindowCount()
|
||||||
|
? null
|
||||||
|
: currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
||||||
// See [Internal: b/65152553].
|
// See [Internal: b/65152553].
|
||||||
@Override
|
@Override
|
||||||
|
@ -73,12 +73,22 @@ import java.util.Map;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
public Window getWindow(
|
||||||
long defaultPositionProjectionUs) {
|
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
|
||||||
long durationUs = durationsUs[windowIndex];
|
long durationUs = durationsUs[windowIndex];
|
||||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||||
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
|
Object tag = setTag ? ids[windowIndex] : null;
|
||||||
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
|
return window.set(
|
||||||
|
tag,
|
||||||
|
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||||
|
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||||
|
/* isSeekable= */ !isDynamic,
|
||||||
|
isDynamic,
|
||||||
|
defaultPositionsUs[windowIndex],
|
||||||
|
durationUs,
|
||||||
|
/* firstPeriodIndex= */ windowIndex,
|
||||||
|
/* lastPeriodIndex= */ windowIndex,
|
||||||
|
/* positionInFirstPeriodUs= */ 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -89,7 +89,7 @@ import com.google.android.gms.cast.MediaTrack;
|
|||||||
case CastStatusCodes.UNKNOWN_ERROR:
|
case CastStatusCodes.UNKNOWN_ERROR:
|
||||||
return "An unknown, unexpected error has occurred.";
|
return "An unknown, unexpected error has occurred.";
|
||||||
default:
|
default:
|
||||||
return "Unknown: " + statusCode;
|
return CastStatusCodes.getStatusCodeString(statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,9 +14,4 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest package="com.google.android.exoplayer2.ext.cast.test"/>
|
||||||
package="com.google.android.exoplayer2.ext.cast.test">
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
@ -280,6 +280,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||||||
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
|
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,17 +353,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||||||
if (!operation.block(readTimeoutMs)) {
|
if (!operation.block(readTimeoutMs)) {
|
||||||
throw new SocketTimeoutException();
|
throw new SocketTimeoutException();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException | SocketTimeoutException e) {
|
} catch (InterruptedException e) {
|
||||||
// If we're timing out or getting interrupted, the operation is still ongoing.
|
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||||
// So we'll need to replace readBuffer to avoid the possibility of it being written to by
|
// operation during a subsequent request.
|
||||||
// this operation during a subsequent request.
|
|
||||||
readBuffer = null;
|
readBuffer = null;
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw new HttpDataSourceException(
|
throw new HttpDataSourceException(
|
||||||
e instanceof InterruptedException
|
new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||||
? new InterruptedIOException((InterruptedException) e)
|
} catch (SocketTimeoutException e) {
|
||||||
: (SocketTimeoutException) e,
|
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||||
currentDataSpec,
|
// operation during a subsequent request.
|
||||||
HttpDataSourceException.TYPE_READ);
|
readBuffer = null;
|
||||||
|
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
|
@ -21,6 +21,7 @@ import android.util.Log;
|
|||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -86,7 +87,7 @@ public final class CronetEngineWrapper {
|
|||||||
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
|
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
|
||||||
CronetEngine cronetEngine = null;
|
CronetEngine cronetEngine = null;
|
||||||
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
|
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
|
||||||
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context);
|
List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
|
||||||
// Remove disabled and fallback Cronet providers from list
|
// Remove disabled and fallback Cronet providers from list
|
||||||
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
|
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
|
||||||
if (!cronetProviders.get(i).isEnabled()
|
if (!cronetProviders.get(i).isEnabled()
|
||||||
|
@ -14,9 +14,4 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest package="com.google.android.exoplayer2.ext.cronet"/>
|
||||||
package="com.google.android.exoplayer2.ext.cronet">
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||||||
*/
|
*/
|
||||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||||
AudioSink audioSink, boolean enableFloatOutput) {
|
AudioSink audioSink, boolean enableFloatOutput) {
|
||||||
super(eventHandler, eventListener, null, false, audioSink);
|
super(
|
||||||
|
eventHandler,
|
||||||
|
eventListener,
|
||||||
|
/* drmSessionManager= */ null,
|
||||||
|
/* playClearSamplesWithoutKeys= */ false,
|
||||||
|
audioSink);
|
||||||
this.enableFloatOutput = enableFloatOutput;
|
this.enableFloatOutput = enableFloatOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -18,8 +18,6 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.google.android.exoplayer2.ext.flac.test">
|
package="com.google.android.exoplayer2.ext.flac.test">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
|
||||||
|
|
||||||
<application android:debuggable="true"
|
<application android:debuggable="true"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||||
|
@ -13,13 +13,13 @@ track 0:
|
|||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
rotationDegrees = -1
|
rotationDegrees = 0
|
||||||
pixelWidthHeightRatio = -1.0
|
pixelWidthHeightRatio = 1.0
|
||||||
channelCount = 2
|
channelCount = 2
|
||||||
sampleRate = 48000
|
sampleRate = 48000
|
||||||
pcmEncoding = 2
|
pcmEncoding = 2
|
||||||
encoderDelay = -1
|
encoderDelay = 0
|
||||||
encoderPadding = -1
|
encoderPadding = 0
|
||||||
subsampleOffsetUs = 9223372036854775807
|
subsampleOffsetUs = 9223372036854775807
|
||||||
selectionFlags = 0
|
selectionFlags = 0
|
||||||
language = null
|
language = null
|
||||||
|
@ -13,13 +13,13 @@ track 0:
|
|||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
rotationDegrees = -1
|
rotationDegrees = 0
|
||||||
pixelWidthHeightRatio = -1.0
|
pixelWidthHeightRatio = 1.0
|
||||||
channelCount = 2
|
channelCount = 2
|
||||||
sampleRate = 48000
|
sampleRate = 48000
|
||||||
pcmEncoding = 2
|
pcmEncoding = 2
|
||||||
encoderDelay = -1
|
encoderDelay = 0
|
||||||
encoderPadding = -1
|
encoderPadding = 0
|
||||||
subsampleOffsetUs = 9223372036854775807
|
subsampleOffsetUs = 9223372036854775807
|
||||||
selectionFlags = 0
|
selectionFlags = 0
|
||||||
language = null
|
language = null
|
||||||
|
@ -13,13 +13,13 @@ track 0:
|
|||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
rotationDegrees = -1
|
rotationDegrees = 0
|
||||||
pixelWidthHeightRatio = -1.0
|
pixelWidthHeightRatio = 1.0
|
||||||
channelCount = 2
|
channelCount = 2
|
||||||
sampleRate = 48000
|
sampleRate = 48000
|
||||||
pcmEncoding = 2
|
pcmEncoding = 2
|
||||||
encoderDelay = -1
|
encoderDelay = 0
|
||||||
encoderPadding = -1
|
encoderPadding = 0
|
||||||
subsampleOffsetUs = 9223372036854775807
|
subsampleOffsetUs = 9223372036854775807
|
||||||
selectionFlags = 0
|
selectionFlags = 0
|
||||||
language = null
|
language = null
|
||||||
|
@ -13,13 +13,13 @@ track 0:
|
|||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
rotationDegrees = -1
|
rotationDegrees = 0
|
||||||
pixelWidthHeightRatio = -1.0
|
pixelWidthHeightRatio = 1.0
|
||||||
channelCount = 2
|
channelCount = 2
|
||||||
sampleRate = 48000
|
sampleRate = 48000
|
||||||
pcmEncoding = 2
|
pcmEncoding = 2
|
||||||
encoderDelay = -1
|
encoderDelay = 0
|
||||||
encoderPadding = -1
|
encoderPadding = 0
|
||||||
subsampleOffsetUs = 9223372036854775807
|
subsampleOffsetUs = 9223372036854775807
|
||||||
selectionFlags = 0
|
selectionFlags = 0
|
||||||
language = null
|
language = null
|
||||||
|
BIN
extensions/flac/src/androidTest/assets/bear_with_id3.flac
Normal file
162
extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
seekMap:
|
||||||
|
isSeekable = true
|
||||||
|
duration = 2741000
|
||||||
|
getPosition(0) = [[timeUs=0, position=55284]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
format:
|
||||||
|
bitrate = 768000
|
||||||
|
id = null
|
||||||
|
containerMimeType = null
|
||||||
|
sampleMimeType = audio/raw
|
||||||
|
maxInputSize = 16384
|
||||||
|
width = -1
|
||||||
|
height = -1
|
||||||
|
frameRate = -1.0
|
||||||
|
rotationDegrees = 0
|
||||||
|
pixelWidthHeightRatio = 1.0
|
||||||
|
channelCount = 2
|
||||||
|
sampleRate = 48000
|
||||||
|
pcmEncoding = 2
|
||||||
|
encoderDelay = 0
|
||||||
|
encoderPadding = 0
|
||||||
|
subsampleOffsetUs = 9223372036854775807
|
||||||
|
selectionFlags = 0
|
||||||
|
language = null
|
||||||
|
drmInitData = -
|
||||||
|
initializationData:
|
||||||
|
total output bytes = 526272
|
||||||
|
sample count = 33
|
||||||
|
sample 0:
|
||||||
|
time = 0
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 61D2C5C2
|
||||||
|
sample 1:
|
||||||
|
time = 85333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash E6D7F214
|
||||||
|
sample 2:
|
||||||
|
time = 170666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 59BF0D5D
|
||||||
|
sample 3:
|
||||||
|
time = 256000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 3625F468
|
||||||
|
sample 4:
|
||||||
|
time = 341333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F66A323
|
||||||
|
sample 5:
|
||||||
|
time = 426666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash CDBAE629
|
||||||
|
sample 6:
|
||||||
|
time = 512000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 536F3A91
|
||||||
|
sample 7:
|
||||||
|
time = 597333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash D4F35C9C
|
||||||
|
sample 8:
|
||||||
|
time = 682666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash EE04CEBF
|
||||||
|
sample 9:
|
||||||
|
time = 768000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 647E2A67
|
||||||
|
sample 10:
|
||||||
|
time = 853333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 31583F2C
|
||||||
|
sample 11:
|
||||||
|
time = 938666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash E433A93D
|
||||||
|
sample 12:
|
||||||
|
time = 1024000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 5E1C7051
|
||||||
|
sample 13:
|
||||||
|
time = 1109333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 43E6E358
|
||||||
|
sample 14:
|
||||||
|
time = 1194666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 5DC1B256
|
||||||
|
sample 15:
|
||||||
|
time = 1280000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 3D9D95CF
|
||||||
|
sample 16:
|
||||||
|
time = 1365333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 2A5BD2C0
|
||||||
|
sample 17:
|
||||||
|
time = 1450666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 93E25061
|
||||||
|
sample 18:
|
||||||
|
time = 1536000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash B81793D8
|
||||||
|
sample 19:
|
||||||
|
time = 1621333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 1A3BD49F
|
||||||
|
sample 20:
|
||||||
|
time = 1706666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash FB672FF1
|
||||||
|
sample 21:
|
||||||
|
time = 1792000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 48AB8B45
|
||||||
|
sample 22:
|
||||||
|
time = 1877333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 13C9640A
|
||||||
|
sample 23:
|
||||||
|
time = 1962666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 499E4A0B
|
||||||
|
sample 24:
|
||||||
|
time = 2048000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F9A783E6
|
||||||
|
sample 25:
|
||||||
|
time = 2133333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash D2B77598
|
||||||
|
sample 26:
|
||||||
|
time = 2218666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash CE5B826C
|
||||||
|
sample 27:
|
||||||
|
time = 2304000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash E99EE956
|
||||||
|
sample 28:
|
||||||
|
time = 2389333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F2DB1486
|
||||||
|
sample 29:
|
||||||
|
time = 2474666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 1636EAB
|
||||||
|
sample 30:
|
||||||
|
time = 2560000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 23457C08
|
||||||
|
sample 31:
|
||||||
|
time = 2645333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 30EB8381
|
||||||
|
sample 32:
|
||||||
|
time = 2730666
|
||||||
|
flags = 1
|
||||||
|
data = length 1984, hash 59CFDE1B
|
||||||
|
tracksEnded = true
|
122
extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
seekMap:
|
||||||
|
isSeekable = true
|
||||||
|
duration = 2741000
|
||||||
|
getPosition(0) = [[timeUs=0, position=55284]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
format:
|
||||||
|
bitrate = 768000
|
||||||
|
id = null
|
||||||
|
containerMimeType = null
|
||||||
|
sampleMimeType = audio/raw
|
||||||
|
maxInputSize = 16384
|
||||||
|
width = -1
|
||||||
|
height = -1
|
||||||
|
frameRate = -1.0
|
||||||
|
rotationDegrees = 0
|
||||||
|
pixelWidthHeightRatio = 1.0
|
||||||
|
channelCount = 2
|
||||||
|
sampleRate = 48000
|
||||||
|
pcmEncoding = 2
|
||||||
|
encoderDelay = 0
|
||||||
|
encoderPadding = 0
|
||||||
|
subsampleOffsetUs = 9223372036854775807
|
||||||
|
selectionFlags = 0
|
||||||
|
language = null
|
||||||
|
drmInitData = -
|
||||||
|
initializationData:
|
||||||
|
total output bytes = 362432
|
||||||
|
sample count = 23
|
||||||
|
sample 0:
|
||||||
|
time = 853333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 31583F2C
|
||||||
|
sample 1:
|
||||||
|
time = 938666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash E433A93D
|
||||||
|
sample 2:
|
||||||
|
time = 1024000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 5E1C7051
|
||||||
|
sample 3:
|
||||||
|
time = 1109333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 43E6E358
|
||||||
|
sample 4:
|
||||||
|
time = 1194666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 5DC1B256
|
||||||
|
sample 5:
|
||||||
|
time = 1280000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 3D9D95CF
|
||||||
|
sample 6:
|
||||||
|
time = 1365333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 2A5BD2C0
|
||||||
|
sample 7:
|
||||||
|
time = 1450666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 93E25061
|
||||||
|
sample 8:
|
||||||
|
time = 1536000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash B81793D8
|
||||||
|
sample 9:
|
||||||
|
time = 1621333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 1A3BD49F
|
||||||
|
sample 10:
|
||||||
|
time = 1706666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash FB672FF1
|
||||||
|
sample 11:
|
||||||
|
time = 1792000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 48AB8B45
|
||||||
|
sample 12:
|
||||||
|
time = 1877333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 13C9640A
|
||||||
|
sample 13:
|
||||||
|
time = 1962666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 499E4A0B
|
||||||
|
sample 14:
|
||||||
|
time = 2048000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F9A783E6
|
||||||
|
sample 15:
|
||||||
|
time = 2133333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash D2B77598
|
||||||
|
sample 16:
|
||||||
|
time = 2218666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash CE5B826C
|
||||||
|
sample 17:
|
||||||
|
time = 2304000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash E99EE956
|
||||||
|
sample 18:
|
||||||
|
time = 2389333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F2DB1486
|
||||||
|
sample 19:
|
||||||
|
time = 2474666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 1636EAB
|
||||||
|
sample 20:
|
||||||
|
time = 2560000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 23457C08
|
||||||
|
sample 21:
|
||||||
|
time = 2645333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 30EB8381
|
||||||
|
sample 22:
|
||||||
|
time = 2730666
|
||||||
|
flags = 1
|
||||||
|
data = length 1984, hash 59CFDE1B
|
||||||
|
tracksEnded = true
|
@ -0,0 +1,78 @@
|
|||||||
|
seekMap:
|
||||||
|
isSeekable = true
|
||||||
|
duration = 2741000
|
||||||
|
getPosition(0) = [[timeUs=0, position=55284]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
format:
|
||||||
|
bitrate = 768000
|
||||||
|
id = null
|
||||||
|
containerMimeType = null
|
||||||
|
sampleMimeType = audio/raw
|
||||||
|
maxInputSize = 16384
|
||||||
|
width = -1
|
||||||
|
height = -1
|
||||||
|
frameRate = -1.0
|
||||||
|
rotationDegrees = 0
|
||||||
|
pixelWidthHeightRatio = 1.0
|
||||||
|
channelCount = 2
|
||||||
|
sampleRate = 48000
|
||||||
|
pcmEncoding = 2
|
||||||
|
encoderDelay = 0
|
||||||
|
encoderPadding = 0
|
||||||
|
subsampleOffsetUs = 9223372036854775807
|
||||||
|
selectionFlags = 0
|
||||||
|
language = null
|
||||||
|
drmInitData = -
|
||||||
|
initializationData:
|
||||||
|
total output bytes = 182208
|
||||||
|
sample count = 12
|
||||||
|
sample 0:
|
||||||
|
time = 1792000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 48AB8B45
|
||||||
|
sample 1:
|
||||||
|
time = 1877333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 13C9640A
|
||||||
|
sample 2:
|
||||||
|
time = 1962666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 499E4A0B
|
||||||
|
sample 3:
|
||||||
|
time = 2048000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F9A783E6
|
||||||
|
sample 4:
|
||||||
|
time = 2133333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash D2B77598
|
||||||
|
sample 5:
|
||||||
|
time = 2218666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash CE5B826C
|
||||||
|
sample 6:
|
||||||
|
time = 2304000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash E99EE956
|
||||||
|
sample 7:
|
||||||
|
time = 2389333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash F2DB1486
|
||||||
|
sample 8:
|
||||||
|
time = 2474666
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 1636EAB
|
||||||
|
sample 9:
|
||||||
|
time = 2560000
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 23457C08
|
||||||
|
sample 10:
|
||||||
|
time = 2645333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 30EB8381
|
||||||
|
sample 11:
|
||||||
|
time = 2730666
|
||||||
|
flags = 1
|
||||||
|
data = length 1984, hash 59CFDE1B
|
||||||
|
tracksEnded = true
|
@ -0,0 +1,38 @@
|
|||||||
|
seekMap:
|
||||||
|
isSeekable = true
|
||||||
|
duration = 2741000
|
||||||
|
getPosition(0) = [[timeUs=0, position=55284]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
format:
|
||||||
|
bitrate = 768000
|
||||||
|
id = null
|
||||||
|
containerMimeType = null
|
||||||
|
sampleMimeType = audio/raw
|
||||||
|
maxInputSize = 16384
|
||||||
|
width = -1
|
||||||
|
height = -1
|
||||||
|
frameRate = -1.0
|
||||||
|
rotationDegrees = 0
|
||||||
|
pixelWidthHeightRatio = 1.0
|
||||||
|
channelCount = 2
|
||||||
|
sampleRate = 48000
|
||||||
|
pcmEncoding = 2
|
||||||
|
encoderDelay = 0
|
||||||
|
encoderPadding = 0
|
||||||
|
subsampleOffsetUs = 9223372036854775807
|
||||||
|
selectionFlags = 0
|
||||||
|
language = null
|
||||||
|
drmInitData = -
|
||||||
|
initializationData:
|
||||||
|
total output bytes = 18368
|
||||||
|
sample count = 2
|
||||||
|
sample 0:
|
||||||
|
time = 2645333
|
||||||
|
flags = 1
|
||||||
|
data = length 16384, hash 30EB8381
|
||||||
|
sample 1:
|
||||||
|
time = 2730666
|
||||||
|
flags = 1
|
||||||
|
data = length 1984, hash 59CFDE1B
|
||||||
|
tracksEnded = true
|
@ -33,7 +33,7 @@ public class FlacExtractorTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testExtractFlacSample() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
new ExtractorFactory() {
|
new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase {
|
|||||||
"bear.flac",
|
"bear.flac",
|
||||||
getInstrumentation().getContext());
|
getInstrumentation().getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testExtractFlacSampleWithId3Header() throws Exception {
|
||||||
|
ExtractorAsserts.assertBehavior(
|
||||||
|
new ExtractorFactory() {
|
||||||
|
@Override
|
||||||
|
public Extractor create() {
|
||||||
|
return new FlacExtractor();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bear_with_id3.flac",
|
||||||
|
getInstrumentation().getContext());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,20 +17,27 @@ package com.google.android.exoplayer2.ext.flac;
|
|||||||
|
|
||||||
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
|
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
|
||||||
|
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.Id3Peeker;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@ -51,22 +58,56 @@ public final class FlacExtractor implements Extractor {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Flags controlling the behavior of the extractor. */
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef(
|
||||||
|
flag = true,
|
||||||
|
value = {FLAG_DISABLE_ID3_METADATA}
|
||||||
|
)
|
||||||
|
public @interface Flags {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
|
||||||
|
* required.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_DISABLE_ID3_METADATA = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
|
* FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
|
||||||
* mandatory STREAMINFO.
|
* mandatory STREAMINFO.
|
||||||
*/
|
*/
|
||||||
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
||||||
|
|
||||||
private ExtractorOutput extractorOutput;
|
private final Id3Peeker id3Peeker;
|
||||||
private TrackOutput trackOutput;
|
private final boolean isId3MetadataDisabled;
|
||||||
|
|
||||||
private FlacDecoderJni decoderJni;
|
private FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
private boolean metadataParsed;
|
private ExtractorOutput extractorOutput;
|
||||||
|
private TrackOutput trackOutput;
|
||||||
|
|
||||||
private ParsableByteArray outputBuffer;
|
private ParsableByteArray outputBuffer;
|
||||||
private ByteBuffer outputByteBuffer;
|
private ByteBuffer outputByteBuffer;
|
||||||
|
|
||||||
|
private Metadata id3Metadata;
|
||||||
|
|
||||||
|
private boolean metadataParsed;
|
||||||
|
|
||||||
|
/** Constructs an instance with flags = 0. */
|
||||||
|
public FlacExtractor() {
|
||||||
|
this(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance.
|
||||||
|
*
|
||||||
|
* @param flags Flags that control the extractor's behavior.
|
||||||
|
*/
|
||||||
|
public FlacExtractor(int flags) {
|
||||||
|
id3Peeker = new Id3Peeker();
|
||||||
|
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
extractorOutput = output;
|
extractorOutput = output;
|
||||||
@ -81,14 +122,19 @@ public final class FlacExtractor implements Extractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
byte[] header = new byte[FLAC_SIGNATURE.length];
|
if (input.getPosition() == 0) {
|
||||||
input.peekFully(header, 0, FLAC_SIGNATURE.length);
|
id3Metadata = peekId3Data(input);
|
||||||
return Arrays.equals(header, FLAC_SIGNATURE);
|
}
|
||||||
|
return peekFlacSignature(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
|
||||||
|
id3Metadata = peekId3Data(input);
|
||||||
|
}
|
||||||
|
|
||||||
decoderJni.setData(input);
|
decoderJni.setData(input);
|
||||||
|
|
||||||
if (!metadataParsed) {
|
if (!metadataParsed) {
|
||||||
@ -112,18 +158,21 @@ public final class FlacExtractor implements Extractor {
|
|||||||
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
||||||
Format mediaFormat =
|
Format mediaFormat =
|
||||||
Format.createAudioSampleFormat(
|
Format.createAudioSampleFormat(
|
||||||
null,
|
/* id= */ null,
|
||||||
MimeTypes.AUDIO_RAW,
|
MimeTypes.AUDIO_RAW,
|
||||||
null,
|
/* codecs= */ null,
|
||||||
streamInfo.bitRate(),
|
streamInfo.bitRate(),
|
||||||
streamInfo.maxDecodedFrameSize(),
|
streamInfo.maxDecodedFrameSize(),
|
||||||
streamInfo.channels,
|
streamInfo.channels,
|
||||||
streamInfo.sampleRate,
|
streamInfo.sampleRate,
|
||||||
getPcmEncoding(streamInfo.bitsPerSample),
|
getPcmEncoding(streamInfo.bitsPerSample),
|
||||||
null,
|
/* encoderDelay= */ 0,
|
||||||
null,
|
/* encoderPadding= */ 0,
|
||||||
0,
|
/* initializationData= */ null,
|
||||||
null);
|
/* drmInitData= */ null,
|
||||||
|
/* selectionFlags= */ 0,
|
||||||
|
/* language= */ null,
|
||||||
|
isId3MetadataDisabled ? null : id3Metadata);
|
||||||
trackOutput.format(mediaFormat);
|
trackOutput.format(mediaFormat);
|
||||||
|
|
||||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||||
@ -170,6 +219,31 @@ public final class FlacExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peeks ID3 tag data (if present) at the beginning of the input.
|
||||||
|
*
|
||||||
|
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
|
||||||
|
* present in the input.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
input.resetPeekPosition();
|
||||||
|
Id3Decoder.FramePredicate id3FramePredicate =
|
||||||
|
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
|
||||||
|
return id3Peeker.peekId3Data(input, id3FramePredicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
|
||||||
|
*
|
||||||
|
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
|
||||||
|
*/
|
||||||
|
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
byte[] header = new byte[FLAC_SIGNATURE.length];
|
||||||
|
input.peekFully(header, 0, FLAC_SIGNATURE.length);
|
||||||
|
return Arrays.equals(header, FLAC_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
private static final class FlacSeekMap implements SeekMap {
|
private static final class FlacSeekMap implements SeekMap {
|
||||||
|
|
||||||
private final long durationUs;
|
private final long durationUs;
|
||||||
|
@ -319,6 +319,8 @@ bool FLACParser::decodeMetadata() {
|
|||||||
case 48000:
|
case 48000:
|
||||||
case 88200:
|
case 88200:
|
||||||
case 96000:
|
case 96000:
|
||||||
|
case 176400:
|
||||||
|
case 192000:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ALOGE("unsupported sample rate %u", getSampleRate());
|
ALOGE("unsupported sample rate %u", getSampleRate());
|
||||||
|
@ -26,6 +26,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
implementation 'com.google.vr:sdk-audio:1.80.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.gvr;
|
package com.google.android.exoplayer2.ext.gvr;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
@ -39,7 +40,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
|
|
||||||
private int sampleRateHz;
|
private int sampleRateHz;
|
||||||
private int channelCount;
|
private int channelCount;
|
||||||
private GvrAudioSurround gvrAudioSurround;
|
@Nullable private GvrAudioSurround gvrAudioSurround;
|
||||||
private ByteBuffer buffer;
|
private ByteBuffer buffer;
|
||||||
private boolean inputEnded;
|
private boolean inputEnded;
|
||||||
|
|
||||||
@ -48,14 +49,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
private float y;
|
private float y;
|
||||||
private float z;
|
private float z;
|
||||||
|
|
||||||
/**
|
/** Creates a new GVR audio processor. */
|
||||||
* Creates a new GVR audio processor.
|
|
||||||
*/
|
|
||||||
public GvrAudioProcessor() {
|
public GvrAudioProcessor() {
|
||||||
// Use the identity for the initial orientation.
|
// Use the identity for the initial orientation.
|
||||||
w = 1f;
|
w = 1f;
|
||||||
sampleRateHz = Format.NO_VALUE;
|
sampleRateHz = Format.NO_VALUE;
|
||||||
channelCount = Format.NO_VALUE;
|
channelCount = Format.NO_VALUE;
|
||||||
|
buffer = EMPTY_BUFFER;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,9 +77,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
@Override
|
@Override
|
||||||
public synchronized boolean configure(int sampleRateHz, int channelCount,
|
public synchronized boolean configure(
|
||||||
@C.Encoding int encoding) throws UnhandledFormatException {
|
int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||||
|
throws UnhandledFormatException {
|
||||||
if (encoding != C.ENCODING_PCM_16BIT) {
|
if (encoding != C.ENCODING_PCM_16BIT) {
|
||||||
maybeReleaseGvrAudioSurround();
|
maybeReleaseGvrAudioSurround();
|
||||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||||
@ -116,7 +118,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
|
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
|
||||||
FRAMES_PER_OUTPUT_BUFFER);
|
FRAMES_PER_OUTPUT_BUFFER);
|
||||||
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
||||||
if (buffer == null) {
|
if (buffer == EMPTY_BUFFER) {
|
||||||
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
||||||
.order(ByteOrder.nativeOrder());
|
.order(ByteOrder.nativeOrder());
|
||||||
}
|
}
|
||||||
@ -179,10 +181,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
@Override
|
@Override
|
||||||
public synchronized void reset() {
|
public synchronized void reset() {
|
||||||
maybeReleaseGvrAudioSurround();
|
maybeReleaseGvrAudioSurround();
|
||||||
|
updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
|
||||||
inputEnded = false;
|
inputEnded = false;
|
||||||
buffer = null;
|
|
||||||
sampleRateHz = Format.NO_VALUE;
|
sampleRateHz = Format.NO_VALUE;
|
||||||
channelCount = Format.NO_VALUE;
|
channelCount = Format.NO_VALUE;
|
||||||
|
buffer = EMPTY_BUFFER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeReleaseGvrAudioSurround() {
|
private void maybeReleaseGvrAudioSurround() {
|
||||||
|
@ -29,12 +29,12 @@ dependencies {
|
|||||||
// This dependency is necessary to force the supportLibraryVersion of
|
// This dependency is necessary to force the supportLibraryVersion of
|
||||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
||||||
// is included via:
|
// is included via:
|
||||||
// com.google.android.gms:play-services-ads:11.4.2
|
// com.google.android.gms:play-services-ads:12.0.0
|
||||||
// |-- com.google.android.gms:play-services-ads-lite:11.4.2
|
// |-- com.google.android.gms:play-services-ads-lite:12.0.0
|
||||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
// |-- com.google.android.gms:play-services-basement:12.0.0
|
||||||
// |-- com.android.support:support-v4:25.2.0
|
// |-- com.android.support:support-v4:26.1.0
|
||||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,18 @@
|
|||||||
|
<?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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.google.android.exoplayer2.ext.ima">
|
package="com.google.android.exoplayer2.ext.ima">
|
||||||
<meta-data android:name="com.google.android.gms.version"
|
<meta-data android:name="com.google.android.gms.version"
|
||||||
|
@ -52,6 +52,8 @@ import com.google.android.exoplayer2.Timeline;
|
|||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
@ -80,7 +82,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private @Nullable ImaSdkSettings imaSdkSettings;
|
private @Nullable ImaSdkSettings imaSdkSettings;
|
||||||
private long vastLoadTimeoutMs;
|
private @Nullable AdEventListener adEventListener;
|
||||||
|
private int vastLoadTimeoutMs;
|
||||||
|
private int mediaLoadTimeoutMs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new builder for {@link ImaAdsLoader}.
|
* Creates a new builder for {@link ImaAdsLoader}.
|
||||||
@ -89,7 +93,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
*/
|
*/
|
||||||
public Builder(Context context) {
|
public Builder(Context context) {
|
||||||
this.context = Assertions.checkNotNull(context);
|
this.context = Assertions.checkNotNull(context);
|
||||||
vastLoadTimeoutMs = C.TIME_UNSET;
|
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
||||||
|
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,6 +111,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a listener for ad events that will be passed to {@link
|
||||||
|
* AdsManager#addAdEventListener(AdEventListener)}.
|
||||||
|
*
|
||||||
|
* @param adEventListener The ad event listener.
|
||||||
|
* @return This builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder setAdEventListener(AdEventListener adEventListener) {
|
||||||
|
this.adEventListener = Assertions.checkNotNull(adEventListener);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the VAST load timeout, in milliseconds.
|
* Sets the VAST load timeout, in milliseconds.
|
||||||
*
|
*
|
||||||
@ -113,12 +130,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
* @return This builder, for convenience.
|
* @return This builder, for convenience.
|
||||||
* @see AdsRequest#setVastLoadTimeout(float)
|
* @see AdsRequest#setVastLoadTimeout(float)
|
||||||
*/
|
*/
|
||||||
public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) {
|
public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) {
|
||||||
Assertions.checkArgument(vastLoadTimeoutMs >= 0);
|
Assertions.checkArgument(vastLoadTimeoutMs >= 0);
|
||||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the ad media load timeout, in milliseconds.
|
||||||
|
*
|
||||||
|
* @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds.
|
||||||
|
* @return This builder, for convenience.
|
||||||
|
* @see AdsRenderingSettings#setLoadVideoTimeout(int)
|
||||||
|
*/
|
||||||
|
public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) {
|
||||||
|
Assertions.checkArgument(mediaLoadTimeoutMs >= 0);
|
||||||
|
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
|
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
|
||||||
*
|
*
|
||||||
@ -128,7 +158,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
* @return The new {@link ImaAdsLoader}.
|
* @return The new {@link ImaAdsLoader}.
|
||||||
*/
|
*/
|
||||||
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
|
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
|
||||||
return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs);
|
return new ImaAdsLoader(
|
||||||
|
context,
|
||||||
|
adTagUri,
|
||||||
|
imaSdkSettings,
|
||||||
|
null,
|
||||||
|
vastLoadTimeoutMs,
|
||||||
|
mediaLoadTimeoutMs,
|
||||||
|
adEventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,7 +176,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
* @return The new {@link ImaAdsLoader}.
|
* @return The new {@link ImaAdsLoader}.
|
||||||
*/
|
*/
|
||||||
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
|
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
|
||||||
return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs);
|
return new ImaAdsLoader(
|
||||||
|
context,
|
||||||
|
null,
|
||||||
|
imaSdkSettings,
|
||||||
|
adsResponse,
|
||||||
|
vastLoadTimeoutMs,
|
||||||
|
mediaLoadTimeoutMs,
|
||||||
|
adEventListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +218,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
|
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
|
||||||
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
|
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
|
||||||
|
|
||||||
|
private static final int TIMEOUT_UNSET = -1;
|
||||||
|
|
||||||
/** The state of ad playback. */
|
/** The state of ad playback. */
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
|
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
|
||||||
@ -193,7 +239,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
|
|
||||||
private final @Nullable Uri adTagUri;
|
private final @Nullable Uri adTagUri;
|
||||||
private final @Nullable String adsResponse;
|
private final @Nullable String adsResponse;
|
||||||
private final long vastLoadTimeoutMs;
|
private final int vastLoadTimeoutMs;
|
||||||
|
private final int mediaLoadTimeoutMs;
|
||||||
|
private final @Nullable AdEventListener adEventListener;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
private final List<VideoAdPlayerCallback> adCallbacks;
|
private final List<VideoAdPlayerCallback> adCallbacks;
|
||||||
private final ImaSdkFactory imaSdkFactory;
|
private final ImaSdkFactory imaSdkFactory;
|
||||||
@ -209,7 +257,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
private VideoProgressUpdate lastAdProgress;
|
private VideoProgressUpdate lastAdProgress;
|
||||||
|
|
||||||
private AdsManager adsManager;
|
private AdsManager adsManager;
|
||||||
private AdErrorEvent pendingAdErrorEvent;
|
private AdLoadException pendingAdLoadError;
|
||||||
private Timeline timeline;
|
private Timeline timeline;
|
||||||
private long contentDurationMs;
|
private long contentDurationMs;
|
||||||
private int podIndexOffset;
|
private int podIndexOffset;
|
||||||
@ -282,7 +330,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
* more information.
|
* more information.
|
||||||
*/
|
*/
|
||||||
public ImaAdsLoader(Context context, Uri adTagUri) {
|
public ImaAdsLoader(Context context, Uri adTagUri) {
|
||||||
this(context, adTagUri, null, null, C.TIME_UNSET);
|
this(
|
||||||
|
context,
|
||||||
|
adTagUri,
|
||||||
|
/* imaSdkSettings= */ null,
|
||||||
|
/* adsResponse= */ null,
|
||||||
|
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
|
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
|
/* adEventListener= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,7 +353,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
|
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
|
||||||
this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET);
|
this(
|
||||||
|
context,
|
||||||
|
adTagUri,
|
||||||
|
imaSdkSettings,
|
||||||
|
/* adsResponse= */ null,
|
||||||
|
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
|
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
|
/* adEventListener= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImaAdsLoader(
|
private ImaAdsLoader(
|
||||||
@ -306,11 +368,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
@Nullable Uri adTagUri,
|
@Nullable Uri adTagUri,
|
||||||
@Nullable ImaSdkSettings imaSdkSettings,
|
@Nullable ImaSdkSettings imaSdkSettings,
|
||||||
@Nullable String adsResponse,
|
@Nullable String adsResponse,
|
||||||
long vastLoadTimeoutMs) {
|
int vastLoadTimeoutMs,
|
||||||
|
int mediaLoadTimeoutMs,
|
||||||
|
@Nullable AdEventListener adEventListener) {
|
||||||
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
||||||
this.adTagUri = adTagUri;
|
this.adTagUri = adTagUri;
|
||||||
this.adsResponse = adsResponse;
|
this.adsResponse = adsResponse;
|
||||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||||
|
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
||||||
|
this.adEventListener = adEventListener;
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
adCallbacks = new ArrayList<>(1);
|
adCallbacks = new ArrayList<>(1);
|
||||||
imaSdkFactory = ImaSdkFactory.getInstance();
|
imaSdkFactory = ImaSdkFactory.getInstance();
|
||||||
@ -361,7 +427,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
} else /* adsResponse != null */ {
|
} else /* adsResponse != null */ {
|
||||||
request.setAdsResponse(adsResponse);
|
request.setAdsResponse(adsResponse);
|
||||||
}
|
}
|
||||||
if (vastLoadTimeoutMs != C.TIME_UNSET) {
|
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||||
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
||||||
}
|
}
|
||||||
request.setAdDisplayContainer(adDisplayContainer);
|
request.setAdDisplayContainer(adDisplayContainer);
|
||||||
@ -466,6 +532,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
this.adsManager = adsManager;
|
this.adsManager = adsManager;
|
||||||
adsManager.addAdErrorListener(this);
|
adsManager.addAdErrorListener(this);
|
||||||
adsManager.addAdEventListener(this);
|
adsManager.addAdEventListener(this);
|
||||||
|
if (adEventListener != null) {
|
||||||
|
adsManager.addAdEventListener(adEventListener);
|
||||||
|
}
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
// If a player is attached already, start playback immediately.
|
// If a player is attached already, start playback immediately.
|
||||||
try {
|
try {
|
||||||
@ -510,13 +579,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
} else if (isAdGroupLoadError(error)) {
|
} else if (isAdGroupLoadError(error)) {
|
||||||
try {
|
try {
|
||||||
handleAdGroupLoadError();
|
handleAdGroupLoadError(error);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
maybeNotifyInternalError("onAdError", e);
|
maybeNotifyInternalError("onAdError", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pendingAdErrorEvent == null) {
|
if (pendingAdLoadError == null) {
|
||||||
pendingAdErrorEvent = adErrorEvent;
|
pendingAdLoadError = AdLoadException.createForAllAds(error);
|
||||||
}
|
}
|
||||||
maybeNotifyPendingAdLoadError();
|
maybeNotifyPendingAdLoadError();
|
||||||
}
|
}
|
||||||
@ -796,6 +865,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
||||||
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
||||||
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
||||||
|
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||||
|
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
// Set up the ad playback state, skipping ads based on the start position as required.
|
// Set up the ad playback state, skipping ads based on the start position as required.
|
||||||
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
|
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
|
||||||
@ -900,9 +972,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
break;
|
break;
|
||||||
case LOG:
|
case LOG:
|
||||||
Map<String, String> adData = adEvent.getAdData();
|
Map<String, String> adData = adEvent.getAdData();
|
||||||
Log.i(TAG, "Log AdEvent: " + adData);
|
String message = "AdEvent: " + adData;
|
||||||
|
Log.i(TAG, message);
|
||||||
if ("adLoadError".equals(adData.get("type"))) {
|
if ("adLoadError".equals(adData.get("type"))) {
|
||||||
handleAdGroupLoadError();
|
handleAdGroupLoadError(new IOException(message));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ALL_ADS_COMPLETED:
|
case ALL_ADS_COMPLETED:
|
||||||
@ -974,7 +1047,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAdGroupLoadError() {
|
private void handleAdGroupLoadError(Exception error) {
|
||||||
int adGroupIndex =
|
int adGroupIndex =
|
||||||
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
|
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
|
||||||
if (adGroupIndex == C.INDEX_UNSET) {
|
if (adGroupIndex == C.INDEX_UNSET) {
|
||||||
@ -996,6 +1069,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
|
if (pendingAdLoadError == null) {
|
||||||
|
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
||||||
@ -1074,21 +1150,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeNotifyPendingAdLoadError() {
|
private void maybeNotifyPendingAdLoadError() {
|
||||||
if (pendingAdErrorEvent != null) {
|
if (pendingAdLoadError != null && eventListener != null) {
|
||||||
if (eventListener != null) {
|
eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri));
|
||||||
eventListener.onAdLoadError(
|
pendingAdLoadError = null;
|
||||||
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
|
|
||||||
}
|
|
||||||
pendingAdErrorEvent = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeNotifyInternalError(String name, Exception cause) {
|
private void maybeNotifyInternalError(String name, Exception cause) {
|
||||||
String message = "Internal error in " + name;
|
String message = "Internal error in " + name;
|
||||||
Log.e(TAG, message, cause);
|
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.
|
// We can't recover from an unexpected error in general, so skip all remaining ads.
|
||||||
if (adPlaybackState == null) {
|
if (adPlaybackState == null) {
|
||||||
adPlaybackState = new AdPlaybackState();
|
adPlaybackState = new AdPlaybackState();
|
||||||
@ -1098,6 +1168,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
|
if (eventListener != null) {
|
||||||
|
eventListener.onAdLoadError(
|
||||||
|
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
|
||||||
|
new DataSpec(adTagUri));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
|
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
|
||||||
|
@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
@ -33,10 +34,12 @@ import java.io.IOException;
|
|||||||
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public final class ImaAdsMediaSource implements MediaSource {
|
public final class ImaAdsMediaSource extends BaseMediaSource {
|
||||||
|
|
||||||
private final AdsMediaSource adsMediaSource;
|
private final AdsMediaSource adsMediaSource;
|
||||||
|
|
||||||
|
private SourceInfoRefreshListener adsMediaSourceListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new source that inserts ads linearly with the content specified by
|
* Constructs a new source that inserts ads linearly with the content specified by
|
||||||
* {@code contentMediaSource}.
|
* {@code contentMediaSource}.
|
||||||
@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareSource(
|
public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
|
||||||
final ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
|
adsMediaSourceListener =
|
||||||
adsMediaSource.prepareSource(
|
new SourceInfoRefreshListener() {
|
||||||
player,
|
|
||||||
isTopLevelSource,
|
|
||||||
new Listener() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onSourceInfoRefreshed(
|
public void onSourceInfoRefreshed(
|
||||||
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||||
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
|
refreshSourceInfo(timeline, manifest);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releaseSource() {
|
public void releaseSourceInternal() {
|
||||||
adsMediaSource.releaseSource();
|
adsMediaSource.releaseSource(adsMediaSourceListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
extensions/jobdispatcher/README.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# ExoPlayer Firebase JobDispatcher extension #
|
||||||
|
|
||||||
|
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
|
||||||
|
|
||||||
|
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
|
||||||
|
|
||||||
|
## Getting the extension ##
|
||||||
|
|
||||||
|
The easiest way to use the extension is to add it as a gradle dependency:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
implementation 'com.google.android.exoplayer:extension-jobdispatcher:2.X.X'
|
||||||
|
```
|
||||||
|
|
||||||
|
where `2.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
|
||||||
|
|
43
extensions/jobdispatcher/build.gradle
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion project.ext.minSdkVersion
|
||||||
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation 'com.firebase:firebase-jobdispatcher:0.8.5'
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
javadocTitle = 'Firebase JobDispatcher extension'
|
||||||
|
}
|
||||||
|
apply from: '../../javadoc_library.gradle'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
releaseArtifact = 'extension-jobdispatcher'
|
||||||
|
releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.'
|
||||||
|
}
|
||||||
|
apply from: '../../publish.gradle'
|
18
extensions/jobdispatcher/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest package="com.google.android.exoplayer2.ext.jobdispatcher"/>
|
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* 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.jobdispatcher;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.firebase.jobdispatcher.Constraint;
|
||||||
|
import com.firebase.jobdispatcher.FirebaseJobDispatcher;
|
||||||
|
import com.firebase.jobdispatcher.GooglePlayDriver;
|
||||||
|
import com.firebase.jobdispatcher.Job;
|
||||||
|
import com.firebase.jobdispatcher.Job.Builder;
|
||||||
|
import com.firebase.jobdispatcher.JobParameters;
|
||||||
|
import com.firebase.jobdispatcher.JobService;
|
||||||
|
import com.firebase.jobdispatcher.Lifetime;
|
||||||
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
|
import com.google.android.exoplayer2.scheduler.Scheduler;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
|
||||||
|
* {@link JobDispatcherSchedulerService} to your manifest:
|
||||||
|
*
|
||||||
|
* <pre>{@literal
|
||||||
|
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
*
|
||||||
|
* <service
|
||||||
|
* android:name="com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"
|
||||||
|
* android:exported="false">
|
||||||
|
* <intent-filter>
|
||||||
|
* <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
|
||||||
|
* </intent-filter>
|
||||||
|
* </service>
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses
|
||||||
|
* should be guarded with a call to {@code
|
||||||
|
* GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
|
||||||
|
*
|
||||||
|
* @see <a
|
||||||
|
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
|
||||||
|
*/
|
||||||
|
public final class JobDispatcherScheduler implements Scheduler {
|
||||||
|
|
||||||
|
private static final String TAG = "JobDispatcherScheduler";
|
||||||
|
private static final String KEY_SERVICE_ACTION = "service_action";
|
||||||
|
private static final String KEY_SERVICE_PACKAGE = "service_package";
|
||||||
|
private static final String KEY_REQUIREMENTS = "requirements";
|
||||||
|
|
||||||
|
private final String jobTag;
|
||||||
|
private final FirebaseJobDispatcher jobDispatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context A context.
|
||||||
|
* @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous
|
||||||
|
* instance, anything scheduled by the previous instance will be canceled by this instance if
|
||||||
|
* {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
|
||||||
|
*/
|
||||||
|
public JobDispatcherScheduler(Context context, String jobTag) {
|
||||||
|
this.jobDispatcher =
|
||||||
|
new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
|
||||||
|
this.jobTag = jobTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
|
||||||
|
Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
|
||||||
|
int result = jobDispatcher.schedule(job);
|
||||||
|
logd("Scheduling job: " + jobTag + " result: " + result);
|
||||||
|
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean cancel() {
|
||||||
|
int result = jobDispatcher.cancel(jobTag);
|
||||||
|
logd("Canceling job: " + jobTag + " result: " + result);
|
||||||
|
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Job buildJob(
|
||||||
|
FirebaseJobDispatcher dispatcher,
|
||||||
|
Requirements requirements,
|
||||||
|
String tag,
|
||||||
|
String serviceAction,
|
||||||
|
String servicePackage) {
|
||||||
|
Builder builder =
|
||||||
|
dispatcher
|
||||||
|
.newJobBuilder()
|
||||||
|
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
|
||||||
|
.setTag(tag);
|
||||||
|
|
||||||
|
switch (requirements.getRequiredNetworkType()) {
|
||||||
|
case Requirements.NETWORK_TYPE_NONE:
|
||||||
|
// do nothing.
|
||||||
|
break;
|
||||||
|
case Requirements.NETWORK_TYPE_ANY:
|
||||||
|
builder.addConstraint(Constraint.ON_ANY_NETWORK);
|
||||||
|
break;
|
||||||
|
case Requirements.NETWORK_TYPE_UNMETERED:
|
||||||
|
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requirements.isIdleRequired()) {
|
||||||
|
builder.addConstraint(Constraint.DEVICE_IDLE);
|
||||||
|
}
|
||||||
|
if (requirements.isChargingRequired()) {
|
||||||
|
builder.addConstraint(Constraint.DEVICE_CHARGING);
|
||||||
|
}
|
||||||
|
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
|
||||||
|
|
||||||
|
Bundle extras = new Bundle();
|
||||||
|
extras.putString(KEY_SERVICE_ACTION, serviceAction);
|
||||||
|
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
|
||||||
|
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
|
||||||
|
builder.setExtras(extras);
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logd(String message) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A {@link JobService} that starts the target service if the requirements are met. */
|
||||||
|
public static final class JobDispatcherSchedulerService extends JobService {
|
||||||
|
@Override
|
||||||
|
public boolean onStartJob(JobParameters params) {
|
||||||
|
logd("JobDispatcherSchedulerService is started");
|
||||||
|
Bundle extras = params.getExtras();
|
||||||
|
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
|
||||||
|
if (requirements.checkRequirements(this)) {
|
||||||
|
logd("Requirements are met");
|
||||||
|
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
|
||||||
|
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
|
||||||
|
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
|
||||||
|
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
|
||||||
|
Util.startForegroundService(this, intent);
|
||||||
|
} else {
|
||||||
|
logd("Requirements are not met");
|
||||||
|
jobFinished(params, /* needsReschedule */ true);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStopJob(JobParameters params) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||||||
|
|
||||||
private @Nullable PlaybackPreparer playbackPreparer;
|
private @Nullable PlaybackPreparer playbackPreparer;
|
||||||
private ControlDispatcher controlDispatcher;
|
private ControlDispatcher controlDispatcher;
|
||||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||||
private boolean hasSurface;
|
private boolean hasSurface;
|
||||||
private boolean lastNotifiedPreparedState;
|
private boolean lastNotifiedPreparedState;
|
||||||
@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||||||
* @param errorMessageProvider The {@link ErrorMessageProvider}.
|
* @param errorMessageProvider The {@link ErrorMessageProvider}.
|
||||||
*/
|
*/
|
||||||
public void setErrorMessageProvider(
|
public void setErrorMessageProvider(
|
||||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||||
this.errorMessageProvider = errorMessageProvider;
|
this.errorMessageProvider = errorMessageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,12 +334,11 @@ public final class MediaSessionConnector {
|
|||||||
private Player player;
|
private Player player;
|
||||||
private CustomActionProvider[] customActionProviders;
|
private CustomActionProvider[] customActionProviders;
|
||||||
private Map<String, CustomActionProvider> customActionMap;
|
private Map<String, CustomActionProvider> customActionMap;
|
||||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||||
private PlaybackPreparer playbackPreparer;
|
private PlaybackPreparer playbackPreparer;
|
||||||
private QueueNavigator queueNavigator;
|
private QueueNavigator queueNavigator;
|
||||||
private QueueEditor queueEditor;
|
private QueueEditor queueEditor;
|
||||||
private RatingCallback ratingCallback;
|
private RatingCallback ratingCallback;
|
||||||
private ExoPlaybackException playbackException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance. Must be called on the same thread that is used to construct the player
|
* Creates an instance. Must be called on the same thread that is used to construct the player
|
||||||
@ -403,16 +402,18 @@ public final class MediaSessionConnector {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the player to be connected to the media session.
|
* Sets the player to be connected to the media session.
|
||||||
* <p>
|
*
|
||||||
* The order in which any {@link CustomActionProvider}s are passed determines the order of the
|
* <p>The order in which any {@link CustomActionProvider}s are passed determines the order of the
|
||||||
* actions published with the playback state of the session.
|
* actions published with the playback state of the session.
|
||||||
*
|
*
|
||||||
* @param player The player to be connected to the {@code MediaSession}.
|
* @param player The player to be connected to the {@code MediaSession}.
|
||||||
* @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
|
* @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
|
||||||
* @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle
|
* @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle
|
||||||
* custom actions.
|
* custom actions.
|
||||||
*/
|
*/
|
||||||
public void setPlayer(Player player, PlaybackPreparer playbackPreparer,
|
public void setPlayer(
|
||||||
|
Player player,
|
||||||
|
@Nullable PlaybackPreparer playbackPreparer,
|
||||||
CustomActionProvider... customActionProviders) {
|
CustomActionProvider... customActionProviders) {
|
||||||
if (this.player != null) {
|
if (this.player != null) {
|
||||||
this.player.removeListener(exoPlayerEventListener);
|
this.player.removeListener(exoPlayerEventListener);
|
||||||
@ -435,13 +436,16 @@ public final class MediaSessionConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link ErrorMessageProvider}.
|
* Sets the optional {@link ErrorMessageProvider}.
|
||||||
*
|
*
|
||||||
* @param errorMessageProvider The error message provider.
|
* @param errorMessageProvider The error message provider.
|
||||||
*/
|
*/
|
||||||
public void setErrorMessageProvider(
|
public void setErrorMessageProvider(
|
||||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||||
|
if (this.errorMessageProvider != errorMessageProvider) {
|
||||||
this.errorMessageProvider = errorMessageProvider;
|
this.errorMessageProvider = errorMessageProvider;
|
||||||
|
updateMediaSessionPlaybackState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -451,10 +455,12 @@ public final class MediaSessionConnector {
|
|||||||
* @param queueNavigator The queue navigator.
|
* @param queueNavigator The queue navigator.
|
||||||
*/
|
*/
|
||||||
public void setQueueNavigator(QueueNavigator queueNavigator) {
|
public void setQueueNavigator(QueueNavigator queueNavigator) {
|
||||||
|
if (this.queueNavigator != queueNavigator) {
|
||||||
unregisterCommandReceiver(this.queueNavigator);
|
unregisterCommandReceiver(this.queueNavigator);
|
||||||
this.queueNavigator = queueNavigator;
|
this.queueNavigator = queueNavigator;
|
||||||
registerCommandReceiver(queueNavigator);
|
registerCommandReceiver(queueNavigator);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link QueueEditor} to handle queue edits sent by the media controller.
|
* Sets the {@link QueueEditor} to handle queue edits sent by the media controller.
|
||||||
@ -462,11 +468,13 @@ public final class MediaSessionConnector {
|
|||||||
* @param queueEditor The queue editor.
|
* @param queueEditor The queue editor.
|
||||||
*/
|
*/
|
||||||
public void setQueueEditor(QueueEditor queueEditor) {
|
public void setQueueEditor(QueueEditor queueEditor) {
|
||||||
|
if (this.queueEditor != queueEditor) {
|
||||||
unregisterCommandReceiver(this.queueEditor);
|
unregisterCommandReceiver(this.queueEditor);
|
||||||
this.queueEditor = queueEditor;
|
this.queueEditor = queueEditor;
|
||||||
registerCommandReceiver(queueEditor);
|
registerCommandReceiver(queueEditor);
|
||||||
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
|
mediaSession.setFlags(
|
||||||
: EDITOR_MEDIA_SESSION_FLAGS);
|
queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -475,10 +483,12 @@ public final class MediaSessionConnector {
|
|||||||
* @param ratingCallback The rating callback.
|
* @param ratingCallback The rating callback.
|
||||||
*/
|
*/
|
||||||
public void setRatingCallback(RatingCallback ratingCallback) {
|
public void setRatingCallback(RatingCallback ratingCallback) {
|
||||||
|
if (this.ratingCallback != ratingCallback) {
|
||||||
unregisterCommandReceiver(this.ratingCallback);
|
unregisterCommandReceiver(this.ratingCallback);
|
||||||
this.ratingCallback = ratingCallback;
|
this.ratingCallback = ratingCallback;
|
||||||
registerCommandReceiver(this.ratingCallback);
|
registerCommandReceiver(this.ratingCallback);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
||||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||||
@ -514,17 +524,17 @@ public final class MediaSessionConnector {
|
|||||||
}
|
}
|
||||||
customActionMap = Collections.unmodifiableMap(currentActions);
|
customActionMap = Collections.unmodifiableMap(currentActions);
|
||||||
|
|
||||||
int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR
|
int playbackState = player.getPlaybackState();
|
||||||
|
ExoPlaybackException playbackError =
|
||||||
|
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
|
||||||
|
int sessionPlaybackState =
|
||||||
|
playbackError != null
|
||||||
|
? PlaybackStateCompat.STATE_ERROR
|
||||||
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
|
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
|
||||||
if (playbackException != null) {
|
if (playbackError != null && errorMessageProvider != null) {
|
||||||
if (errorMessageProvider != null) {
|
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
|
||||||
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackException);
|
|
||||||
builder.setErrorMessage(message.first, message.second);
|
builder.setErrorMessage(message.first, message.second);
|
||||||
}
|
}
|
||||||
if (player.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
playbackException = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
|
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
|
||||||
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
@ -674,12 +684,8 @@ public final class MediaSessionConnector {
|
|||||||
// active queue item and queue navigation actions may need to be updated
|
// active queue item and queue navigation actions may need to be updated
|
||||||
updateMediaSessionPlaybackState();
|
updateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
if (currentWindowCount != windowCount) {
|
|
||||||
// active queue item and queue navigation actions may need to be updated
|
|
||||||
updateMediaSessionPlaybackState();
|
|
||||||
}
|
|
||||||
currentWindowCount = windowCount;
|
currentWindowCount = windowCount;
|
||||||
currentWindowIndex = player.getCurrentWindowIndex();
|
currentWindowIndex = windowIndex;
|
||||||
updateMediaSessionMetadata();
|
updateMediaSessionMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -703,12 +709,6 @@ public final class MediaSessionConnector {
|
|||||||
updateMediaSessionPlaybackState();
|
updateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError(ExoPlaybackException error) {
|
|
||||||
playbackException = error;
|
|
||||||
updateMediaSessionPlaybackState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||||
if (currentWindowIndex != player.getCurrentWindowIndex()) {
|
if (currentWindowIndex != player.getCurrentWindowIndex()) {
|
||||||
|
@ -24,21 +24,21 @@ import android.support.v4.media.session.MediaControllerCompat;
|
|||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link MediaSessionConnector.QueueEditor} implementation based on the
|
* A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
|
||||||
* {@link DynamicConcatenatingMediaSource}.
|
* ConcatenatingMediaSource}.
|
||||||
* <p>
|
*
|
||||||
* This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
|
* <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
|
||||||
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
|
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
|
||||||
* This allows to move the currently playing window without interrupting playback.
|
* This allows to move the currently playing window without interrupting playback.
|
||||||
*/
|
*/
|
||||||
public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor,
|
public final class TimelineQueueEditor
|
||||||
MediaSessionConnector.CommandReceiver {
|
implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver {
|
||||||
|
|
||||||
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
|
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
|
||||||
public static final String EXTRA_FROM_INDEX = "from_index";
|
public static final String EXTRA_FROM_INDEX = "from_index";
|
||||||
@ -124,20 +124,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
|
|||||||
private final QueueDataAdapter queueDataAdapter;
|
private final QueueDataAdapter queueDataAdapter;
|
||||||
private final MediaSourceFactory sourceFactory;
|
private final MediaSourceFactory sourceFactory;
|
||||||
private final MediaDescriptionEqualityChecker equalityChecker;
|
private final MediaDescriptionEqualityChecker equalityChecker;
|
||||||
private final DynamicConcatenatingMediaSource queueMediaSource;
|
private final ConcatenatingMediaSource queueMediaSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||||
*
|
*
|
||||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||||
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
|
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||||
* manipulate.
|
|
||||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||||
*/
|
*/
|
||||||
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
|
public TimelineQueueEditor(
|
||||||
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
|
@NonNull MediaControllerCompat mediaController,
|
||||||
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) {
|
@NonNull ConcatenatingMediaSource queueMediaSource,
|
||||||
|
@NonNull QueueDataAdapter queueDataAdapter,
|
||||||
|
@NonNull MediaSourceFactory sourceFactory) {
|
||||||
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
|
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
|
||||||
new MediaIdEqualityChecker());
|
new MediaIdEqualityChecker());
|
||||||
}
|
}
|
||||||
@ -146,15 +147,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
|
|||||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||||
*
|
*
|
||||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||||
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
|
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||||
* manipulate.
|
|
||||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||||
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
|
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
|
||||||
*/
|
*/
|
||||||
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
|
public TimelineQueueEditor(
|
||||||
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
|
@NonNull MediaControllerCompat mediaController,
|
||||||
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory,
|
@NonNull ConcatenatingMediaSource queueMediaSource,
|
||||||
|
@NonNull QueueDataAdapter queueDataAdapter,
|
||||||
|
@NonNull MediaSourceFactory sourceFactory,
|
||||||
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
|
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
|
||||||
this.mediaController = mediaController;
|
this.mediaController = mediaController;
|
||||||
this.queueMediaSource = queueMediaSource;
|
this.queueMediaSource = queueMediaSource;
|
||||||
|
@ -73,10 +73,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||||||
/**
|
/**
|
||||||
* Gets the {@link MediaDescriptionCompat} for a given timeline window index.
|
* Gets the {@link MediaDescriptionCompat} for a given timeline window index.
|
||||||
*
|
*
|
||||||
|
* @param player The current player.
|
||||||
* @param windowIndex The timeline window index for which to provide a description.
|
* @param windowIndex The timeline window index for which to provide a description.
|
||||||
* @return A {@link MediaDescriptionCompat}.
|
* @return A {@link MediaDescriptionCompat}.
|
||||||
*/
|
*/
|
||||||
public abstract MediaDescriptionCompat getMediaDescription(int windowIndex);
|
public abstract MediaDescriptionCompat getMediaDescription(Player player, int windowIndex);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSupportedQueueNavigatorActions(Player player) {
|
public long getSupportedQueueNavigatorActions(Player player) {
|
||||||
@ -185,7 +186,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||||||
windowCount - queueSize);
|
windowCount - queueSize);
|
||||||
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||||
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
||||||
queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i));
|
queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(player, i), i));
|
||||||
}
|
}
|
||||||
mediaSession.setQueue(queue);
|
mediaSession.setQueue(queue);
|
||||||
activeQueueItemId = currentWindowIndex;
|
activeQueueItemId = currentWindowIndex;
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Herhaal niks</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Herhaal een</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Herhaal alles</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">ምንም አትድገም</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">አንድ ድገም</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">ሁሉንም ድገም</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">عدم التكرار</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">تكرار مقطع صوتي واحد</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">تكرار الكل</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
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="exo_media_action_repeat_all_description">"Bütün təkrarlayın"</string>
|
|
||||||
<string name="exo_media_action_repeat_one_description">"Təkrar bir"</string>
|
|
||||||
<string name="exo_media_action_repeat_off_description">"Heç bir təkrar"</string>
|
|
||||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Heç biri təkrarlanmasın</string>
|
||||||
|
<string name="exo_media_action_repeat_one_description">Biri təkrarlansın</string>
|
||||||
|
<string name="exo_media_action_repeat_all_description">Hamısı təkrarlansın</string>
|
||||||
|
</resources>
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ne ponavljaj nijednu</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Ponovi jednu</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Ponovi sve</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
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="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>
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Не паўтараць нічога</string>
|
||||||
|
<string name="exo_media_action_repeat_one_description">Паўтарыць адзін элемент</string>
|
||||||
|
<string name="exo_media_action_repeat_all_description">Паўтарыць усе</string>
|
||||||
|
</resources>
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Без повтаряне</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Повтаряне на един елемент</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Повтаряне на всички</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
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="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>
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">কোনও আইটেম আবার চালাবেন না</string>
|
||||||
|
<string name="exo_media_action_repeat_one_description">একটি আইটেম আবার চালান</string>
|
||||||
|
<string name="exo_media_action_repeat_all_description">সবগুলি আইটেম আবার চালান</string>
|
||||||
|
</resources>
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
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="exo_media_action_repeat_all_description">"Ponovite sve"</string>
|
|
||||||
<string name="exo_media_action_repeat_off_description">"Ne ponavljaju"</string>
|
|
||||||
<string name="exo_media_action_repeat_one_description">"Ponovite jedan"</string>
|
|
||||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ne ponavljaj</string>
|
||||||
|
<string name="exo_media_action_repeat_one_description">Ponovi jedno</string>
|
||||||
|
<string name="exo_media_action_repeat_all_description">Ponovi sve</string>
|
||||||
|
</resources>
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">No en repeteixis cap</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Repeteix una</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Repeteix tot</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Neopakovat</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Opakovat jednu</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Opakovat vše</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Gentag ingen</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Gentag én</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Gentag alle</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Keinen wiederholen</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Einen wiederholen</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Alle wiederholen</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Καμία επανάληψη</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Επανάληψη ενός κομματιού</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Επανάληψη όλων</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Repeat none</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Repeat one</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Repeat all</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Repeat none</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Repeat one</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Repeat all</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Repeat none</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Repeat one</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Repeat all</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">No repetir</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Repetir uno</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Repetir todo</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">No repetir</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Repetir uno</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Repetir todo</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
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="exo_media_action_repeat_all_description">"Korda kõike"</string>
|
|
||||||
<string name="exo_media_action_repeat_off_description">"Ära korda midagi"</string>
|
|
||||||
<string name="exo_media_action_repeat_one_description">"Korda ühte"</string>
|
|
||||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ära korda ühtegi</string>
|
||||||
|
<string name="exo_media_action_repeat_one_description">Korda ühte</string>
|
||||||
|
<string name="exo_media_action_repeat_all_description">Korda kõiki</string>
|
||||||
|
</resources>
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
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="exo_media_action_repeat_all_description">"Errepikatu guztiak"</string>
|
|
||||||
<string name="exo_media_action_repeat_off_description">"Ez errepikatu"</string>
|
|
||||||
<string name="exo_media_action_repeat_one_description">"Errepikatu bat"</string>
|
|
||||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ez errepikatu</string>
|
||||||
|
<string name="exo_media_action_repeat_one_description">Errepikatu bat</string>
|
||||||
|
<string name="exo_media_action_repeat_all_description">Errepikatu guztiak</string>
|
||||||
|
</resources>
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">تکرار هیچکدام</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">یکبار تکرار</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">تکرار همه</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ei uudelleentoistoa</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Toista yksi uudelleen</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Toista kaikki uudelleen</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Lire une chanson en boucle</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|
@ -1,22 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
<resources>
|
||||||
|
<string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<string name="exo_media_action_repeat_one_description">Lire un titre en boucle</string>
|
||||||
you may not use this file except in compliance with the License.
|
<string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
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>
|
</resources>
|
||||||
|