4
.gitignore
vendored
@ -43,6 +43,9 @@ cmake-build-debug
|
||||
dist
|
||||
tmp
|
||||
|
||||
# External native builds
|
||||
.externalNativeBuild
|
||||
|
||||
# VP9 extension
|
||||
extensions/vp9/src/main/jni/libvpx
|
||||
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||
@ -62,3 +65,4 @@ extensions/cronet/jniLibs/*
|
||||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!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
|
||||
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
|
||||
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
|
||||
module. The following will add a dependency to the full library:
|
||||
Next add a dependency in the `build.gradle` file of your app module. The
|
||||
following will add a dependency to the full library:
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||
|
117
RELEASENOTES.md
@ -1,5 +1,119 @@
|
||||
# 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 ###
|
||||
|
||||
* Fix ProGuard configuration for Cast, IMA and OkHttp extensions.
|
||||
@ -93,7 +207,7 @@
|
||||
([#3630](https://github.com/google/ExoPlayer/issues/3630)).
|
||||
* DASH:
|
||||
* Support in-band Emsg events targeting the player with scheme id
|
||||
"urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3".
|
||||
`urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3".
|
||||
* Support EventStream elements in DASH manifests.
|
||||
* HLS:
|
||||
* Add opt-in support for chunkless preparation in HLS. This allows an
|
||||
@ -163,6 +277,7 @@
|
||||
([#3792](https://github.com/google/ExoPlayer/issues/3792).
|
||||
* Support 14-bit mode and little endianness in DTS PES packets
|
||||
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
|
||||
* Demo app: Add ability to download not DRM protected content.
|
||||
|
||||
### 2.6.1 ###
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<!-- 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.
|
||||
@ -14,8 +12,8 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Ulang semua"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Tiada ulangan"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Ulangan"</string>
|
||||
</resources>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore path="**/checker-qual-*.jar"/>
|
||||
</issue>
|
||||
</lint>
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.7.3'
|
||||
releaseVersionCode = 2703
|
||||
releaseVersion = '2.8.0'
|
||||
releaseVersionCode = 2800
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
@ -25,12 +25,14 @@ project.ext {
|
||||
buildToolsVersion = '27.0.3'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '27.0.0'
|
||||
playServicesLibraryVersion = '11.4.2'
|
||||
playServicesLibraryVersion = '12.0.0'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
junitVersion = '4.12'
|
||||
truthVersion = '0.39'
|
||||
robolectricVersion = '3.7.1'
|
||||
autoValueVersion = '1.6'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
@ -36,6 +36,7 @@ include modulePrefix + 'extension-opus'
|
||||
include modulePrefix + 'extension-vp9'
|
||||
include modulePrefix + 'extension-rtmp'
|
||||
include modulePrefix + 'extension-leanback'
|
||||
include modulePrefix + 'extension-jobdispatcher'
|
||||
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
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-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||
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')
|
||||
&& 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.castdemo.DemoUtil.Sample;
|
||||
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.MediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
@ -80,8 +80,8 @@ import java.util.ArrayList;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<DemoUtil.Sample> mediaQueue;
|
||||
private final QueuePositionListener queuePositionListener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
|
||||
private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
|
||||
private boolean castMediaQueueCreationPending;
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
@ -117,9 +117,10 @@ import java.util.ArrayList;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
@ -155,9 +156,8 @@ import java.util.ArrayList;
|
||||
*/
|
||||
public void addItem(Sample sample) {
|
||||
mediaQueue.add(sample);
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
||||
} else {
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
||||
if (currentPlayer == castPlayer) {
|
||||
castPlayer.addItems(buildMediaQueueItem(sample));
|
||||
}
|
||||
}
|
||||
@ -186,9 +186,8 @@ import java.util.ArrayList;
|
||||
* @return Whether the removal was successful.
|
||||
*/
|
||||
public boolean removeItem(int itemIndex) {
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
} else {
|
||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
if (currentPlayer == castPlayer) {
|
||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||
@ -215,9 +214,8 @@ import java.util.ArrayList;
|
||||
*/
|
||||
public boolean moveItem(int fromIndex, int toIndex) {
|
||||
// Player update.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
} else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
int periodCount = castTimeline.getPeriodCount();
|
||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||
@ -263,6 +261,7 @@ import java.util.ArrayList;
|
||||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
concatenatingMediaSource.clear();
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
localPlayerView.setPlayer(null);
|
||||
@ -354,11 +353,7 @@ import java.util.ArrayList;
|
||||
// Media queue management.
|
||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
||||
if (currentPlayer == exoPlayer) {
|
||||
dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource();
|
||||
for (int i = 0; i < mediaQueue.size(); i++) {
|
||||
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
|
||||
}
|
||||
exoPlayer.prepare(dynamicConcatenatingMediaSource);
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
|
||||
// Playback transition.
|
||||
|
@ -17,8 +17,6 @@ package com.google.android.exoplayer2.imademo;
|
||||
|
||||
import android.content.Context;
|
||||
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.ContentType;
|
||||
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.source.ExtractorMediaSource;
|
||||
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.dash.DashMediaSource;
|
||||
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).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource =
|
||||
buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null);
|
||||
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds =
|
||||
@ -121,9 +117,8 @@ import com.google.android.exoplayer2.util.Util;
|
||||
// AdsMediaSource.MediaSourceFactory implementation.
|
||||
|
||||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
return buildMediaSource(uri, handler, listener);
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return buildMediaSource(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -134,25 +129,22 @@ import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private MediaSource buildMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
private MediaSource buildMediaSource(Uri uri) {
|
||||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
manifestDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
@ -19,6 +19,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<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.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk/>
|
||||
@ -73,6 +75,18 @@
|
||||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
@ -578,5 +578,16 @@
|
||||
"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;
|
||||
|
||||
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.DefaultDataSourceFactory;
|
||||
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.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 java.io.File;
|
||||
|
||||
/**
|
||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||
*/
|
||||
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;
|
||||
|
||||
private File downloadDirectory;
|
||||
private Cache downloadCache;
|
||||
private DownloadManager downloadManager;
|
||||
private DownloadTracker downloadTracker;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@ -38,7 +69,9 @@ public class DemoApplication extends Application {
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
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}. */
|
||||
@ -47,8 +80,69 @@ public class DemoApplication extends Application {
|
||||
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
||||
}
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
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;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
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.SimpleExoPlayer;
|
||||
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.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
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.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
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.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.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.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.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.DefaultTrackSelector;
|
||||
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.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
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.DefaultBandwidthMeter;
|
||||
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.Util;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
@ -86,10 +95,10 @@ public class PlayerActivity extends Activity
|
||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION = "drm_multi_session";
|
||||
public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
|
||||
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
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 EXTENSION_EXTRA = "extension";
|
||||
@ -98,11 +107,22 @@ public class PlayerActivity extends Activity
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
public static final String URI_LIST_EXTRA = "uri_list";
|
||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
||||
|
||||
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";
|
||||
|
||||
// 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 CookieManager DEFAULT_COOKIE_MANAGER;
|
||||
static {
|
||||
@ -110,23 +130,21 @@ public class PlayerActivity extends Activity
|
||||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
}
|
||||
|
||||
private Handler mainHandler;
|
||||
private EventLogger eventLogger;
|
||||
private PlayerView playerView;
|
||||
private LinearLayout debugRootView;
|
||||
private TextView debugTextView;
|
||||
|
||||
private DataSource.Factory mediaDataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private MediaSource mediaSource;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private TrackSelectionHelper trackSelectionHelper;
|
||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
private DebugTextViewHelper debugViewHelper;
|
||||
private boolean inErrorState;
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
|
||||
private boolean shouldAutoPlay;
|
||||
private int resumeWindow;
|
||||
private long resumePosition;
|
||||
private boolean startAutoPlay;
|
||||
private int startWindow;
|
||||
private long startPosition;
|
||||
|
||||
// Fields used only for ad playback. The ads loader is loaded via reflection.
|
||||
|
||||
@ -139,10 +157,7 @@ public class PlayerActivity extends Activity
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
shouldAutoPlay = true;
|
||||
clearResumePosition();
|
||||
mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||
mainHandler = new Handler();
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
@ -155,14 +170,24 @@ public class PlayerActivity extends Activity
|
||||
|
||||
playerView = findViewById(R.id.player_view);
|
||||
playerView.setControllerVisibilityListener(this);
|
||||
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||
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
|
||||
public void onNewIntent(Intent intent) {
|
||||
releasePlayer();
|
||||
shouldAutoPlay = true;
|
||||
clearResumePosition();
|
||||
clearStartPosition();
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
@ -207,7 +232,12 @@ public class PlayerActivity extends Activity
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@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();
|
||||
} else {
|
||||
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
|
||||
|
||||
@Override
|
||||
@ -230,8 +270,19 @@ public class PlayerActivity extends Activity
|
||||
if (view.getParent() == debugRootView) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
trackSelectionHelper.showSelectionDialog(
|
||||
this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag());
|
||||
CharSequence title = ((Button) view).getText();
|
||||
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
|
||||
|
||||
private void initializePlayer() {
|
||||
if (player == null) {
|
||||
Intent intent = getIntent();
|
||||
boolean needNewPlayer = player == null;
|
||||
if (needNewPlayer) {
|
||||
TrackSelection.Factory adaptiveTrackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
||||
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
|
||||
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
|
||||
lastSeenTrackGroupArray = null;
|
||||
eventLogger = new EventLogger(trackSelector);
|
||||
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));
|
||||
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)) {
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
|
||||
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
|
||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);
|
||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);
|
||||
int errorStringId = R.string.error_drm_unknown;
|
||||
if (Util.SDK_INT < 18) {
|
||||
errorStringId = R.string.error_drm_not_supported;
|
||||
@ -290,62 +360,53 @@ public class PlayerActivity extends Activity
|
||||
}
|
||||
if (drmSessionManager == null) {
|
||||
showToast(errorStringId);
|
||||
finish();
|
||||
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 =
|
||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
|
||||
drmSessionManager, extensionRendererMode);
|
||||
DefaultRenderersFactory renderersFactory =
|
||||
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(eventLogger);
|
||||
player.addMetadataOutput(eventLogger);
|
||||
player.addAudioDebugListener(eventLogger);
|
||||
player.addVideoDebugListener(eventLogger);
|
||||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
playerView.setPlayer(player);
|
||||
playerView.setPlaybackPreparer(this);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
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];
|
||||
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]
|
||||
: new ConcatenatingMediaSource(mediaSources);
|
||||
mediaSource =
|
||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
||||
if (adTagUriString != null) {
|
||||
Uri adTagUri = Uri.parse(adTagUriString);
|
||||
@ -362,82 +423,105 @@ public class PlayerActivity extends Activity
|
||||
} else {
|
||||
releaseAdsLoader();
|
||||
}
|
||||
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
||||
if (haveResumePosition) {
|
||||
player.seekTo(resumeWindow, resumePosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveResumePosition, false);
|
||||
inErrorState = false;
|
||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||
if (haveStartPosition) {
|
||||
player.seekTo(startWindow, startPosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveStartPosition, false);
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(
|
||||
Uri uri,
|
||||
String overrideExtension,
|
||||
@Nullable Handler handler,
|
||||
@Nullable MediaSourceEventListener listener) {
|
||||
@ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
|
||||
: Util.inferContentType("." + overrideExtension);
|
||||
private MediaSource buildMediaSource(Uri uri) {
|
||||
return buildMediaSource(uri, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
.createMediaSource(uri, handler, listener);
|
||||
.setManifestParser(
|
||||
new FilteringManifestParser<>(
|
||||
new DashManifestParser(), (List<RepresentationKey>) getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
.createMediaSource(uri, handler, listener);
|
||||
.setManifestParser(
|
||||
new FilteringManifestParser<>(
|
||||
new SsManifestParser(), (List<StreamKey>) getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
.setPlaylistParser(
|
||||
new FilteringManifestParser<>(
|
||||
new HlsPlaylistParser(), (List<RenditionKey>) getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(UUID uuid,
|
||||
String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
private List<?> getOfflineStreamKeys(Uri uri) {
|
||||
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
||||
}
|
||||
|
||||
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
||||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
throws UnsupportedDrmException {
|
||||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
|
||||
buildHttpDataSourceFactory(false));
|
||||
HttpDataSource.Factory licenseDataSourceFactory =
|
||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
||||
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
|
||||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
|
||||
null, mainHandler, eventLogger, multiSession);
|
||||
return new DefaultDrmSessionManager<>(
|
||||
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
if (player != null) {
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
debugViewHelper.stop();
|
||||
debugViewHelper = null;
|
||||
shouldAutoPlay = player.getPlayWhenReady();
|
||||
updateResumePosition();
|
||||
player.release();
|
||||
player = null;
|
||||
mediaSource = null;
|
||||
trackSelector = null;
|
||||
trackSelectionHelper = null;
|
||||
eventLogger = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateResumePosition() {
|
||||
resumeWindow = player.getCurrentWindowIndex();
|
||||
resumePosition = Math.max(0, player.getContentPosition());
|
||||
private void updateTrackSelectorParameters() {
|
||||
if (trackSelector != null) {
|
||||
trackSelectorParameters = trackSelector.getParameters();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearResumePosition() {
|
||||
resumeWindow = C.INDEX_UNSET;
|
||||
resumePosition = C.TIME_UNSET;
|
||||
private void updateStartPosition() {
|
||||
if (player != null) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
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.
|
||||
@ -486,10 +558,8 @@ public class PlayerActivity extends Activity
|
||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||
new AdsMediaSource.MediaSourceFactory() {
|
||||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
return PlayerActivity.this.buildMediaSource(
|
||||
uri, /* overrideExtension= */ null, handler, listener);
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return PlayerActivity.this.buildMediaSource(uri);
|
||||
}
|
||||
|
||||
@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 AdsMediaSource(
|
||||
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
|
||||
return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// IMA extension not loaded.
|
||||
return null;
|
||||
@ -529,20 +598,20 @@ public class PlayerActivity extends Activity
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < mappedTrackInfo.length; i++) {
|
||||
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
|
||||
if (trackGroups.length != 0) {
|
||||
Button button = new Button(this);
|
||||
int label;
|
||||
switch (player.getRendererType(i)) {
|
||||
case C.TRACK_TYPE_AUDIO:
|
||||
label = R.string.audio;
|
||||
label = R.string.exo_track_selection_title_audio;
|
||||
break;
|
||||
case C.TRACK_TYPE_VIDEO:
|
||||
label = R.string.video;
|
||||
label = R.string.exo_track_selection_title_video;
|
||||
break;
|
||||
case C.TRACK_TYPE_TEXT:
|
||||
label = R.string.text;
|
||||
label = R.string.exo_track_selection_title_text;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
@ -593,48 +662,20 @@ public class PlayerActivity extends Activity
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (inErrorState) {
|
||||
// This will only occur if the user has performed a seek whilst in the error state. Update
|
||||
// the resume position so that if the user then retries, playback will resume from the
|
||||
// position to which they seeked.
|
||||
updateResumePosition();
|
||||
if (player.getPlaybackError() != null) {
|
||||
// The user has performed a seek whilst in the error state. Update the resume position so
|
||||
// that if the user then retries, playback resumes from the position to which they seeked.
|
||||
updateStartPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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)) {
|
||||
clearResumePosition();
|
||||
clearStartPosition();
|
||||
initializePlayer();
|
||||
} else {
|
||||
updateResumePosition();
|
||||
updateStartPosition();
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
@ -647,11 +688,11 @@ public class PlayerActivity extends Activity
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
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) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
@ -659,7 +700,40 @@ public class PlayerActivity extends Activity
|
||||
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.util.JsonReader;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.ExpandableListView;
|
||||
import android.widget.ExpandableListView.OnChildClickListener;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
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.DataSourceInputStream;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
@ -44,20 +46,27 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An activity for selecting from a list of samples.
|
||||
*/
|
||||
public class SampleChooserActivity extends Activity {
|
||||
/** An activity for selecting from a list of media samples. */
|
||||
public class SampleChooserActivity extends Activity
|
||||
implements DownloadTracker.Listener, OnChildClickListener {
|
||||
|
||||
private static final String TAG = "SampleChooserActivity";
|
||||
|
||||
private DownloadTracker downloadTracker;
|
||||
private SampleAdapter sampleAdapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
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();
|
||||
String dataUri = intent.getDataString();
|
||||
String[] uris;
|
||||
@ -80,8 +89,32 @@ public class SampleChooserActivity extends Activity {
|
||||
uriList.toArray(uris);
|
||||
Arrays.sort(uris);
|
||||
}
|
||||
|
||||
downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker();
|
||||
SampleListLoader loaderTask = new SampleListLoader();
|
||||
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) {
|
||||
@ -89,20 +122,44 @@ public class SampleChooserActivity extends Activity {
|
||||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
ExpandableListView sampleList = findViewById(R.id.sample_list);
|
||||
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;
|
||||
}
|
||||
});
|
||||
sampleAdapter.setSampleGroups(groups);
|
||||
}
|
||||
|
||||
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));
|
||||
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>> {
|
||||
@ -176,15 +233,16 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||
String sampleName = null;
|
||||
String uri = null;
|
||||
Uri uri = null;
|
||||
String extension = null;
|
||||
UUID drmUuid = null;
|
||||
String drmScheme = null;
|
||||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
boolean drmMultiSession = false;
|
||||
boolean preferExtensionDecoders = false;
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
String abrAlgorithm = null;
|
||||
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
@ -194,16 +252,14 @@ public class SampleChooserActivity extends Activity {
|
||||
sampleName = reader.nextString();
|
||||
break;
|
||||
case "uri":
|
||||
uri = reader.nextString();
|
||||
uri = Uri.parse(reader.nextString());
|
||||
break;
|
||||
case "extension":
|
||||
extension = reader.nextString();
|
||||
break;
|
||||
case "drm_scheme":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
||||
String drmScheme = reader.nextString();
|
||||
drmUuid = Util.getDrmUuid(drmScheme);
|
||||
Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme);
|
||||
drmScheme = reader.nextString();
|
||||
break;
|
||||
case "drm_license_url":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
@ -242,21 +298,28 @@ public class SampleChooserActivity extends Activity {
|
||||
case "ad_tag_uri":
|
||||
adTagUri = reader.nextString();
|
||||
break;
|
||||
case "abr_algorithm":
|
||||
Assertions.checkState(
|
||||
!insidePlaylist, "Invalid attribute on nested item: abr_algorithm");
|
||||
abrAlgorithm = reader.nextString();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl,
|
||||
drmKeyRequestProperties, drmMultiSession);
|
||||
DrmInfo drmInfo =
|
||||
drmScheme == null
|
||||
? null
|
||||
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
|
||||
if (playlistSamples != null) {
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
||||
new UriSample[playlistSamples.size()]);
|
||||
return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo,
|
||||
playlistSamplesArray);
|
||||
return new PlaylistSample(
|
||||
sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray);
|
||||
} else {
|
||||
return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension,
|
||||
adTagUri);
|
||||
return new UriSample(
|
||||
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 final List<SampleGroup> sampleGroups;
|
||||
private List<SampleGroup> sampleGroups;
|
||||
|
||||
public SampleAdapter(Context context, List<SampleGroup> sampleGroups) {
|
||||
this.context = context;
|
||||
public SampleAdapter() {
|
||||
sampleGroups = Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setSampleGroups(List<SampleGroup> sampleGroups) {
|
||||
this.sampleGroups = sampleGroups;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -298,10 +364,12 @@ public class SampleChooserActivity extends Activity {
|
||||
View convertView, ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent,
|
||||
false);
|
||||
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, 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;
|
||||
}
|
||||
|
||||
@ -325,8 +393,9 @@ public class SampleChooserActivity extends Activity {
|
||||
ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1,
|
||||
parent, false);
|
||||
view =
|
||||
getLayoutInflater()
|
||||
.inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
|
||||
}
|
||||
((TextView) view).setText(getGroup(groupPosition).title);
|
||||
return view;
|
||||
@ -347,6 +416,25 @@ public class SampleChooserActivity extends Activity {
|
||||
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 {
|
||||
@ -362,14 +450,17 @@ public class SampleChooserActivity extends Activity {
|
||||
}
|
||||
|
||||
private static final class DrmInfo {
|
||||
public final UUID drmSchemeUuid;
|
||||
public final String drmScheme;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean drmMultiSession) {
|
||||
this.drmSchemeUuid = drmSchemeUuid;
|
||||
public DrmInfo(
|
||||
String drmScheme,
|
||||
String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties,
|
||||
boolean drmMultiSession) {
|
||||
this.drmScheme = drmScheme;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
@ -377,31 +468,34 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
public void updateIntent(Intent intent) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString());
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
|
||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);
|
||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class Sample {
|
||||
public final String name;
|
||||
public final boolean preferExtensionDecoders;
|
||||
public final String abrAlgorithm;
|
||||
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.preferExtensionDecoders = preferExtensionDecoders;
|
||||
this.abrAlgorithm = abrAlgorithm;
|
||||
this.drmInfo = drmInfo;
|
||||
}
|
||||
|
||||
public Intent buildIntent(Context context) {
|
||||
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) {
|
||||
drmInfo.updateIntent(intent);
|
||||
}
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@ -409,13 +503,19 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
private static final class UriSample extends Sample {
|
||||
|
||||
public final String uri;
|
||||
public final Uri uri;
|
||||
public final String extension;
|
||||
public final String adTagUri;
|
||||
|
||||
public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri,
|
||||
String extension, String adTagUri) {
|
||||
super(name, preferExtensionDecoders, drmInfo);
|
||||
public UriSample(
|
||||
String name,
|
||||
boolean preferExtensionDecoders,
|
||||
String abrAlgorithm,
|
||||
DrmInfo drmInfo,
|
||||
Uri uri,
|
||||
String extension,
|
||||
String adTagUri) {
|
||||
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.adTagUri = adTagUri;
|
||||
@ -424,7 +524,7 @@ public class SampleChooserActivity extends Activity {
|
||||
@Override
|
||||
public Intent buildIntent(Context context) {
|
||||
return super.buildIntent(context)
|
||||
.setData(Uri.parse(uri))
|
||||
.setData(uri)
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||
.setAction(PlayerActivity.ACTION_VIEW);
|
||||
@ -436,9 +536,13 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo,
|
||||
public PlaylistSample(
|
||||
String name,
|
||||
boolean preferExtensionDecoders,
|
||||
String abrAlgorithm,
|
||||
DrmInfo drmInfo,
|
||||
UriSample... children) {
|
||||
super(name, preferExtensionDecoders, drmInfo);
|
||||
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@ -447,7 +551,7 @@ public class SampleChooserActivity extends Activity {
|
||||
String[] uris = new String[children.length];
|
||||
String[] extensions = new String[children.length];
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
uris[i] = children[i].uri;
|
||||
uris[i] = children[i].uri.toString();
|
||||
extensions[i] = children[i].extension;
|
||||
}
|
||||
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"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?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.
|
||||
@ -14,8 +13,7 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Non repetir"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repetir un"</string>
|
||||
</resources>
|
||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/representation_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
@ -17,19 +17,11 @@
|
||||
|
||||
<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="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>
|
||||
|
||||
@ -55,4 +47,14 @@
|
||||
|
||||
<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>
|
||||
|
@ -30,9 +30,9 @@ dependencies {
|
||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
||||
// used, for example:
|
||||
// com.google.android.gms:play-services-cast-framework:11.4.2
|
||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
// com.google.android.gms:play-services-cast-framework:12.0.0
|
||||
// |-- com.google.android.gms:play-services-basement:12.0.0
|
||||
// |-- com.android.support:support-v4:26.1.0
|
||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
api 'com.android.support:appcompat-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.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
@ -307,6 +308,11 @@ public final class CastPlayer implements Player {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExoPlaybackException getPlaybackError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
if (remoteMediaClient == null) {
|
||||
@ -481,6 +487,14 @@ public final class CastPlayer implements Player {
|
||||
: 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.
|
||||
// See [Internal: b/65152553].
|
||||
@Override
|
||||
|
@ -73,12 +73,22 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
public Window getWindow(
|
||||
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
|
||||
long durationUs = durationsUs[windowIndex];
|
||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
|
||||
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
|
||||
Object tag = setTag ? ids[windowIndex] : null;
|
||||
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
|
||||
|
@ -89,7 +89,7 @@ import com.google.android.gms.cast.MediaTrack;
|
||||
case CastStatusCodes.UNKNOWN_ERROR:
|
||||
return "An unknown, unexpected error has occurred.";
|
||||
default:
|
||||
return "Unknown: " + statusCode;
|
||||
return CastStatusCodes.getStatusCodeString(statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,4 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.ext.cast.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
</manifest>
|
||||
<manifest package="com.google.android.exoplayer2.ext.cast.test"/>
|
||||
|
@ -280,6 +280,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
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)) {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
} catch (InterruptedException | SocketTimeoutException e) {
|
||||
// If we're timing out or getting interrupted, the operation is still ongoing.
|
||||
// So we'll need to replace readBuffer to avoid the possibility of it being written to by
|
||||
// this operation during a subsequent request.
|
||||
} catch (InterruptedException e) {
|
||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
Thread.currentThread().interrupt();
|
||||
throw new HttpDataSourceException(
|
||||
e instanceof InterruptedException
|
||||
? new InterruptedIOException((InterruptedException) e)
|
||||
: (SocketTimeoutException) e,
|
||||
currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
} catch (SocketTimeoutException e) {
|
||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
|
@ -21,6 +21,7 @@ import android.util.Log;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@ -86,7 +87,7 @@ public final class CronetEngineWrapper {
|
||||
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
|
||||
CronetEngine cronetEngine = null;
|
||||
@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
|
||||
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
|
||||
if (!cronetProviders.get(i).isEnabled()
|
||||
|
@ -14,9 +14,4 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
</manifest>
|
||||
<manifest package="com.google.android.exoplayer2.ext.cronet"/>
|
||||
|
@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink, boolean enableFloatOutput) {
|
||||
super(eventHandler, eventListener, null, false, audioSink);
|
||||
super(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
audioSink);
|
||||
this.enableFloatOutput = enableFloatOutput;
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -18,8 +18,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.flac.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
|
@ -13,13 +13,13 @@ track 0:
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
|
@ -13,13 +13,13 @@ track 0:
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
|
@ -13,13 +13,13 @@ track 0:
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
|
@ -13,13 +13,13 @@ track 0:
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
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(
|
||||
new ExtractorFactory() {
|
||||
@Override
|
||||
@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase {
|
||||
"bear.flac",
|
||||
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 android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
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.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||
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.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.nio.ByteBuffer;
|
||||
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
|
||||
* mandatory STREAMINFO.
|
||||
*/
|
||||
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
||||
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput trackOutput;
|
||||
private final Id3Peeker id3Peeker;
|
||||
private final boolean isId3MetadataDisabled;
|
||||
|
||||
private FlacDecoderJni decoderJni;
|
||||
|
||||
private boolean metadataParsed;
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput trackOutput;
|
||||
|
||||
private ParsableByteArray outputBuffer;
|
||||
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
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
@ -81,14 +122,19 @@ public final class FlacExtractor implements Extractor {
|
||||
|
||||
@Override
|
||||
public boolean sniff(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);
|
||||
if (input.getPosition() == 0) {
|
||||
id3Metadata = peekId3Data(input);
|
||||
}
|
||||
return peekFlacSignature(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
|
||||
id3Metadata = peekId3Data(input);
|
||||
}
|
||||
|
||||
decoderJni.setData(input);
|
||||
|
||||
if (!metadataParsed) {
|
||||
@ -112,18 +158,21 @@ public final class FlacExtractor implements Extractor {
|
||||
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
null,
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
null,
|
||||
/* codecs= */ null,
|
||||
streamInfo.bitRate(),
|
||||
streamInfo.maxDecodedFrameSize(),
|
||||
streamInfo.channels,
|
||||
streamInfo.sampleRate,
|
||||
getPcmEncoding(streamInfo.bitsPerSample),
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null);
|
||||
/* encoderDelay= */ 0,
|
||||
/* encoderPadding= */ 0,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null,
|
||||
isId3MetadataDisabled ? null : id3Metadata);
|
||||
trackOutput.format(mediaFormat);
|
||||
|
||||
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 final long durationUs;
|
||||
|
@ -319,6 +319,8 @@ bool FLACParser::decodeMetadata() {
|
||||
case 48000:
|
||||
case 88200:
|
||||
case 96000:
|
||||
case 176400:
|
||||
case 192000:
|
||||
break;
|
||||
default:
|
||||
ALOGE("unsupported sample rate %u", getSampleRate());
|
||||
|
@ -26,6 +26,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.gvr;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
@ -39,7 +40,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
|
||||
private int sampleRateHz;
|
||||
private int channelCount;
|
||||
private GvrAudioSurround gvrAudioSurround;
|
||||
@Nullable private GvrAudioSurround gvrAudioSurround;
|
||||
private ByteBuffer buffer;
|
||||
private boolean inputEnded;
|
||||
|
||||
@ -48,14 +49,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
private float y;
|
||||
private float z;
|
||||
|
||||
/**
|
||||
* Creates a new GVR audio processor.
|
||||
*/
|
||||
/** Creates a new GVR audio processor. */
|
||||
public GvrAudioProcessor() {
|
||||
// Use the identity for the initial orientation.
|
||||
w = 1f;
|
||||
sampleRateHz = Format.NO_VALUE;
|
||||
channelCount = Format.NO_VALUE;
|
||||
buffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,9 +77,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@Override
|
||||
public synchronized boolean configure(int sampleRateHz, int channelCount,
|
||||
@C.Encoding int encoding) throws UnhandledFormatException {
|
||||
public synchronized boolean configure(
|
||||
int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||
throws UnhandledFormatException {
|
||||
if (encoding != C.ENCODING_PCM_16BIT) {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
@ -116,7 +118,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
|
||||
FRAMES_PER_OUTPUT_BUFFER);
|
||||
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
||||
if (buffer == null) {
|
||||
if (buffer == EMPTY_BUFFER) {
|
||||
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
||||
.order(ByteOrder.nativeOrder());
|
||||
}
|
||||
@ -179,10 +181,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||
@Override
|
||||
public synchronized void reset() {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
|
||||
inputEnded = false;
|
||||
buffer = null;
|
||||
sampleRateHz = Format.NO_VALUE;
|
||||
channelCount = Format.NO_VALUE;
|
||||
buffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
private void maybeReleaseGvrAudioSurround() {
|
||||
|
@ -29,12 +29,12 @@ dependencies {
|
||||
// This dependency is necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
||||
// is included via:
|
||||
// com.google.android.gms:play-services-ads:11.4.2
|
||||
// |-- com.google.android.gms:play-services-ads-lite:11.4.2
|
||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
// com.google.android.gms:play-services-ads:12.0.0
|
||||
// |-- com.google.android.gms:play-services-ads-lite:12.0.0
|
||||
// |-- com.google.android.gms:play-services-basement:12.0.0
|
||||
// |-- com.android.support:support-v4:26.1.0
|
||||
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 '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"
|
||||
package="com.google.android.exoplayer2.ext.ima">
|
||||
<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.AdState;
|
||||
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.MimeTypes;
|
||||
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 @Nullable ImaSdkSettings imaSdkSettings;
|
||||
private long vastLoadTimeoutMs;
|
||||
private @Nullable AdEventListener adEventListener;
|
||||
private int vastLoadTimeoutMs;
|
||||
private int mediaLoadTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates a new builder for {@link ImaAdsLoader}.
|
||||
@ -89,7 +93,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
*/
|
||||
public Builder(Context 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@ -113,12 +130,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
* @return This builder, for convenience.
|
||||
* @see AdsRequest#setVastLoadTimeout(float)
|
||||
*/
|
||||
public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) {
|
||||
public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) {
|
||||
Assertions.checkArgument(vastLoadTimeoutMs >= 0);
|
||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||
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.
|
||||
*
|
||||
@ -128,7 +158,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
* @return The new {@link ImaAdsLoader}.
|
||||
*/
|
||||
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}.
|
||||
*/
|
||||
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:"
|
||||
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
|
||||
|
||||
private static final int TIMEOUT_UNSET = -1;
|
||||
|
||||
/** The state of ad playback. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@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 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 List<VideoAdPlayerCallback> adCallbacks;
|
||||
private final ImaSdkFactory imaSdkFactory;
|
||||
@ -209,7 +257,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
private VideoProgressUpdate lastAdProgress;
|
||||
|
||||
private AdsManager adsManager;
|
||||
private AdErrorEvent pendingAdErrorEvent;
|
||||
private AdLoadException pendingAdLoadError;
|
||||
private Timeline timeline;
|
||||
private long contentDurationMs;
|
||||
private int podIndexOffset;
|
||||
@ -282,7 +330,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
* more information.
|
||||
*/
|
||||
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
|
||||
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(
|
||||
@ -306,11 +368,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
@Nullable Uri adTagUri,
|
||||
@Nullable ImaSdkSettings imaSdkSettings,
|
||||
@Nullable String adsResponse,
|
||||
long vastLoadTimeoutMs) {
|
||||
int vastLoadTimeoutMs,
|
||||
int mediaLoadTimeoutMs,
|
||||
@Nullable AdEventListener adEventListener) {
|
||||
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
||||
this.adTagUri = adTagUri;
|
||||
this.adsResponse = adsResponse;
|
||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
||||
this.adEventListener = adEventListener;
|
||||
period = new Timeline.Period();
|
||||
adCallbacks = new ArrayList<>(1);
|
||||
imaSdkFactory = ImaSdkFactory.getInstance();
|
||||
@ -361,7 +427,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
} else /* adsResponse != null */ {
|
||||
request.setAdsResponse(adsResponse);
|
||||
}
|
||||
if (vastLoadTimeoutMs != C.TIME_UNSET) {
|
||||
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
||||
}
|
||||
request.setAdDisplayContainer(adDisplayContainer);
|
||||
@ -466,6 +532,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
this.adsManager = adsManager;
|
||||
adsManager.addAdErrorListener(this);
|
||||
adsManager.addAdEventListener(this);
|
||||
if (adEventListener != null) {
|
||||
adsManager.addAdEventListener(adEventListener);
|
||||
}
|
||||
if (player != null) {
|
||||
// If a player is attached already, start playback immediately.
|
||||
try {
|
||||
@ -510,13 +579,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
updateAdPlaybackState();
|
||||
} else if (isAdGroupLoadError(error)) {
|
||||
try {
|
||||
handleAdGroupLoadError();
|
||||
handleAdGroupLoadError(error);
|
||||
} catch (Exception e) {
|
||||
maybeNotifyInternalError("onAdError", e);
|
||||
}
|
||||
}
|
||||
if (pendingAdErrorEvent == null) {
|
||||
pendingAdErrorEvent = adErrorEvent;
|
||||
if (pendingAdLoadError == null) {
|
||||
pendingAdLoadError = AdLoadException.createForAllAds(error);
|
||||
}
|
||||
maybeNotifyPendingAdLoadError();
|
||||
}
|
||||
@ -796,6 +865,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
||||
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
||||
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.
|
||||
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
|
||||
@ -900,9 +972,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
break;
|
||||
case LOG:
|
||||
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"))) {
|
||||
handleAdGroupLoadError();
|
||||
handleAdGroupLoadError(new IOException(message));
|
||||
}
|
||||
break;
|
||||
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 =
|
||||
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
@ -996,6 +1069,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
}
|
||||
}
|
||||
updateAdPlaybackState();
|
||||
if (pendingAdLoadError == null) {
|
||||
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if (pendingAdErrorEvent != null) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdLoadError(
|
||||
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
|
||||
}
|
||||
pendingAdErrorEvent = null;
|
||||
if (pendingAdLoadError != null && eventListener != null) {
|
||||
eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri));
|
||||
pendingAdLoadError = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyInternalError(String name, Exception cause) {
|
||||
String message = "Internal error in " + name;
|
||||
Log.e(TAG, message, cause);
|
||||
if (eventListener != null) {
|
||||
eventListener.onInternalAdLoadError(new RuntimeException(message, cause));
|
||||
}
|
||||
// We can't recover from an unexpected error in general, so skip all remaining ads.
|
||||
if (adPlaybackState == null) {
|
||||
adPlaybackState = new AdPlaybackState();
|
||||
@ -1098,6 +1168,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
}
|
||||
}
|
||||
updateAdPlaybackState();
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdLoadError(
|
||||
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
|
||||
new DataSpec(adTagUri));
|
||||
}
|
||||
}
|
||||
|
||||
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
|
||||
|
@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
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
|
||||
public final class ImaAdsMediaSource implements MediaSource {
|
||||
public final class ImaAdsMediaSource extends BaseMediaSource {
|
||||
|
||||
private final AdsMediaSource adsMediaSource;
|
||||
|
||||
private SourceInfoRefreshListener adsMediaSourceListener;
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
* {@code contentMediaSource}.
|
||||
@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(
|
||||
final ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
|
||||
adsMediaSource.prepareSource(
|
||||
player,
|
||||
isTopLevelSource,
|
||||
new Listener() {
|
||||
public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
|
||||
adsMediaSourceListener =
|
||||
new SourceInfoRefreshListener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(
|
||||
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
|
||||
refreshSourceInfo(timeline, manifest);
|
||||
}
|
||||
});
|
||||
};
|
||||
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
adsMediaSource.releaseSource();
|
||||
public void releaseSourceInternal() {
|
||||
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 ControlDispatcher controlDispatcher;
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||
private boolean hasSurface;
|
||||
private boolean lastNotifiedPreparedState;
|
||||
@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||
* @param errorMessageProvider The {@link ErrorMessageProvider}.
|
||||
*/
|
||||
public void setErrorMessageProvider(
|
||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
this.errorMessageProvider = errorMessageProvider;
|
||||
}
|
||||
|
||||
|
@ -334,12 +334,11 @@ public final class MediaSessionConnector {
|
||||
private Player player;
|
||||
private CustomActionProvider[] customActionProviders;
|
||||
private Map<String, CustomActionProvider> customActionMap;
|
||||
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private PlaybackPreparer playbackPreparer;
|
||||
private QueueNavigator queueNavigator;
|
||||
private QueueEditor queueEditor;
|
||||
private RatingCallback ratingCallback;
|
||||
private ExoPlaybackException playbackException;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* <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.
|
||||
*
|
||||
* @param player The player to be connected to the {@code MediaSession}.
|
||||
* @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.
|
||||
*/
|
||||
public void setPlayer(Player player, PlaybackPreparer playbackPreparer,
|
||||
public void setPlayer(
|
||||
Player player,
|
||||
@Nullable PlaybackPreparer playbackPreparer,
|
||||
CustomActionProvider... customActionProviders) {
|
||||
if (this.player != null) {
|
||||
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.
|
||||
*/
|
||||
public void setErrorMessageProvider(
|
||||
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
if (this.errorMessageProvider != errorMessageProvider) {
|
||||
this.errorMessageProvider = errorMessageProvider;
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -451,10 +455,12 @@ public final class MediaSessionConnector {
|
||||
* @param queueNavigator The queue navigator.
|
||||
*/
|
||||
public void setQueueNavigator(QueueNavigator queueNavigator) {
|
||||
if (this.queueNavigator != queueNavigator) {
|
||||
unregisterCommandReceiver(this.queueNavigator);
|
||||
this.queueNavigator = queueNavigator;
|
||||
registerCommandReceiver(queueNavigator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void setQueueEditor(QueueEditor queueEditor) {
|
||||
if (this.queueEditor != queueEditor) {
|
||||
unregisterCommandReceiver(this.queueEditor);
|
||||
this.queueEditor = queueEditor;
|
||||
registerCommandReceiver(queueEditor);
|
||||
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
|
||||
: EDITOR_MEDIA_SESSION_FLAGS);
|
||||
mediaSession.setFlags(
|
||||
queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -475,10 +483,12 @@ public final class MediaSessionConnector {
|
||||
* @param ratingCallback The rating callback.
|
||||
*/
|
||||
public void setRatingCallback(RatingCallback ratingCallback) {
|
||||
if (this.ratingCallback != ratingCallback) {
|
||||
unregisterCommandReceiver(this.ratingCallback);
|
||||
this.ratingCallback = ratingCallback;
|
||||
registerCommandReceiver(this.ratingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||
@ -514,17 +524,17 @@ public final class MediaSessionConnector {
|
||||
}
|
||||
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());
|
||||
if (playbackException != null) {
|
||||
if (errorMessageProvider != null) {
|
||||
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackException);
|
||||
if (playbackError != null && errorMessageProvider != null) {
|
||||
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
|
||||
builder.setErrorMessage(message.first, message.second);
|
||||
}
|
||||
if (player.getPlaybackState() != Player.STATE_IDLE) {
|
||||
playbackException = null;
|
||||
}
|
||||
}
|
||||
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
|
||||
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
Bundle extras = new Bundle();
|
||||
@ -674,12 +684,8 @@ public final class MediaSessionConnector {
|
||||
// active queue item and queue navigation actions may need to be updated
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
if (currentWindowCount != windowCount) {
|
||||
// active queue item and queue navigation actions may need to be updated
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
currentWindowCount = windowCount;
|
||||
currentWindowIndex = player.getCurrentWindowIndex();
|
||||
currentWindowIndex = windowIndex;
|
||||
updateMediaSessionMetadata();
|
||||
}
|
||||
|
||||
@ -703,12 +709,6 @@ public final class MediaSessionConnector {
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
playbackException = error;
|
||||
updateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (currentWindowIndex != player.getCurrentWindowIndex()) {
|
||||
|
@ -24,21 +24,21 @@ import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.util.Util;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link MediaSessionConnector.QueueEditor} implementation based on the
|
||||
* {@link DynamicConcatenatingMediaSource}.
|
||||
* <p>
|
||||
* This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
|
||||
* A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
|
||||
* ConcatenatingMediaSource}.
|
||||
*
|
||||
* <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.
|
||||
* This allows to move the currently playing window without interrupting playback.
|
||||
*/
|
||||
public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor,
|
||||
MediaSessionConnector.CommandReceiver {
|
||||
public final class TimelineQueueEditor
|
||||
implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver {
|
||||
|
||||
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
|
||||
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 MediaSourceFactory sourceFactory;
|
||||
private final MediaDescriptionEqualityChecker equalityChecker;
|
||||
private final DynamicConcatenatingMediaSource queueMediaSource;
|
||||
private final ConcatenatingMediaSource queueMediaSource;
|
||||
|
||||
/**
|
||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||
*
|
||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
|
||||
* manipulate.
|
||||
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||
*/
|
||||
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
|
||||
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) {
|
||||
public TimelineQueueEditor(
|
||||
@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull ConcatenatingMediaSource queueMediaSource,
|
||||
@NonNull QueueDataAdapter queueDataAdapter,
|
||||
@NonNull MediaSourceFactory sourceFactory) {
|
||||
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
|
||||
new MediaIdEqualityChecker());
|
||||
}
|
||||
@ -146,15 +147,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
|
||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||
*
|
||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
|
||||
* manipulate.
|
||||
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
|
||||
*/
|
||||
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
|
||||
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory,
|
||||
public TimelineQueueEditor(
|
||||
@NonNull MediaControllerCompat mediaController,
|
||||
@NonNull ConcatenatingMediaSource queueMediaSource,
|
||||
@NonNull QueueDataAdapter queueDataAdapter,
|
||||
@NonNull MediaSourceFactory sourceFactory,
|
||||
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
|
||||
this.mediaController = mediaController;
|
||||
this.queueMediaSource = queueMediaSource;
|
||||
|
@ -73,10 +73,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
||||
/**
|
||||
* 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.
|
||||
* @return A {@link MediaDescriptionCompat}.
|
||||
*/
|
||||
public abstract MediaDescriptionCompat getMediaDescription(int windowIndex);
|
||||
public abstract MediaDescriptionCompat getMediaDescription(Player player, int windowIndex);
|
||||
|
||||
@Override
|
||||
public long getSupportedQueueNavigatorActions(Player player) {
|
||||
@ -185,7 +186,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
||||
windowCount - queueSize);
|
||||
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||
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);
|
||||
activeQueueItemId = currentWindowIndex;
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Herhaal niks</string>
|
||||
<string name="exo_media_action_repeat_one_description">Herhaal een</string>
|
||||
<string name="exo_media_action_repeat_all_description">Herhaal alles</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?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"?>
|
||||
<!-- 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 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>
|
||||
<?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">"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"?>
|
||||
<!-- 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 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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Ne ponavljaj nijednu</string>
|
||||
<string name="exo_media_action_repeat_one_description">Ponovi jednu</string>
|
||||
<string name="exo_media_action_repeat_all_description">Ponovi sve</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">"Паўтарыць усё"</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"?>
|
||||
<!-- 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 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>
|
||||
<?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">"সবগুলির পুনরাবৃত্তি করুন"</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"?>
|
||||
<!-- 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 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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">No en repeteixis cap</string>
|
||||
<string name="exo_media_action_repeat_one_description">Repeteix una</string>
|
||||
<string name="exo_media_action_repeat_all_description">Repeteix tot</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Neopakovat</string>
|
||||
<string name="exo_media_action_repeat_one_description">Opakovat jednu</string>
|
||||
<string name="exo_media_action_repeat_all_description">Opakovat vše</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Gentag ingen</string>
|
||||
<string name="exo_media_action_repeat_one_description">Gentag én</string>
|
||||
<string name="exo_media_action_repeat_all_description">Gentag alle</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Keinen wiederholen</string>
|
||||
<string name="exo_media_action_repeat_one_description">Einen wiederholen</string>
|
||||
<string name="exo_media_action_repeat_all_description">Alle wiederholen</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?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"?>
|
||||
<!-- 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 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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Repeat none</string>
|
||||
<string name="exo_media_action_repeat_one_description">Repeat one</string>
|
||||
<string name="exo_media_action_repeat_all_description">Repeat all</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Repeat none</string>
|
||||
<string name="exo_media_action_repeat_one_description">Repeat one</string>
|
||||
<string name="exo_media_action_repeat_all_description">Repeat all</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Repeat none</string>
|
||||
<string name="exo_media_action_repeat_one_description">Repeat one</string>
|
||||
<string name="exo_media_action_repeat_all_description">Repeat all</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">No repetir</string>
|
||||
<string name="exo_media_action_repeat_one_description">Repetir uno</string>
|
||||
<string name="exo_media_action_repeat_all_description">Repetir todo</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">No repetir</string>
|
||||
<string name="exo_media_action_repeat_one_description">Repetir uno</string>
|
||||
<string name="exo_media_action_repeat_all_description">Repetir todo</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">"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"?>
|
||||
<!-- 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 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>
|
||||
<?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"?>
|
||||
<!-- 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 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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Ei uudelleentoistoa</string>
|
||||
<string name="exo_media_action_repeat_one_description">Toista yksi uudelleen</string>
|
||||
<string name="exo_media_action_repeat_all_description">Toista kaikki uudelleen</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
|
||||
<string name="exo_media_action_repeat_one_description">Lire une chanson en boucle</string>
|
||||
<string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
|
||||
</resources>
|
||||
|
@ -1,22 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
|
||||
<string name="exo_media_action_repeat_one_description">Lire un titre en boucle</string>
|
||||
<string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
|
||||
</resources>
|
||||
|