Merge pull request #4219 from google/dev-v2-r2.8.0

r2.8.0
This commit is contained in:
ojw28 2018-05-08 00:20:32 -07:00 committed by GitHub
commit 000f3f2305
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
725 changed files with 34034 additions and 13445 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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".

View File

@ -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'

View File

@ -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 ###

View File

@ -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>

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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);
}

View File

@ -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>

View File

@ -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"
}
]
}
]

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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() {}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View 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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -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"/>

View File

@ -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) {

View File

@ -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()

View File

@ -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"/>

View File

@ -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;
}

View File

@ -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 {

View File

@ -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">

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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());

View File

@ -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'
}

View File

@ -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() {

View File

@ -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
}

View File

@ -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"

View File

@ -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) {

View File

@ -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);
}
}

View 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

View 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'

View 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"/>

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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()) {

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More