Merge remote-tracking branch 'upstream/dev-v2' into dev-v2

This commit is contained in:
Justin Yorke 2018-05-08 13:28:46 -07:00
commit d709df8e1a
No known key found for this signature in database
GPG Key ID: F14A1056CB1757BB
403 changed files with 12887 additions and 8596 deletions

View File

@ -2,19 +2,19 @@
### dev-v2 (not yet released) ### ### dev-v2 (not yet released) ###
* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within * Coming soon...
a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
* Moved initial bitrate estimate from `AdaptiveTrackSelection` to ### 2.8.0 ###
`DefaultBandwidthMeter`.
* Updated default max buffer length in `DefaultLoadControl`. * Downloading:
* Added `AnalyticsListener` interface which can be registered in * Add `DownloadService`, `DownloadManager` and related classes
`SimpleExoPlayer` to receive detailed meta data for each ExoPlayer event. ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on
* UI components: using these components to download progressive formats can be found
* Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95).
([#3736](https://github.com/google/ExoPlayer/issues/3736)). To see how to download DASH, HLS and SmoothStreaming media, take a look at
* Add PlayerNotificationManager. the app.
* Downloading: Add `DownloadService`, `DownloadManager` and * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and
related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). progressive media.
* MediaSources: * MediaSources:
* Allow reusing media sources after they have been released and * Allow reusing media sources after they have been released and
also in parallel to allow adding them multiple times to a concatenation. also in parallel to allow adding them multiple times to a concatenation.
@ -31,9 +31,23 @@
* Support live stream clipping with `ClippingMediaSource`. * Support live stream clipping with `ClippingMediaSource`.
* Allow setting tags for all media sources in their factories. The tag of the * Allow setting tags for all media sources in their factories. The tag of the
current window can be retrieved with `ExoPlayer.getCurrentTag`. current window can be retrieved with `ExoPlayer.getCurrentTag`.
* IMA: Allow setting the ad media load timeout * UI components:
([#3691](https://github.com/google/ExoPlayer/issues/3691)). * 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: * Audio:
* Support extracting data from AMR container formats, including both narrow
and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)).
* FLAC: * FLAC:
* Sniff FLAC files correctly if they have ID3 headers * Sniff FLAC files correctly if they have ID3 headers
([#4055](https://github.com/google/ExoPlayer/issues/4055)). ([#4055](https://github.com/google/ExoPlayer/issues/4055)).
@ -48,14 +62,17 @@
* Fix an issue where playback of TrueHD streams would get stuck after seeking * Fix an issue where playback of TrueHD streams would get stuck after seeking
due to not finding a syncframe due to not finding a syncframe
((#3845)[https://github.com/google/ExoPlayer/issues/3845]). ((#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 * 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. 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 * Allow trimming more than one sample when applying an elst audio edit via
gapless playback info. gapless playback info.
* Allow overriding skipping/scaling with custom `AudioProcessor`s
((#3142)[https://github.com/google/ExoPlayer/issues/3142]).
* Caching: * Caching:
* Add release method to Cache interface. * Add release method to the `Cache` interface, and prevent multiple instances
* Prevent multiple instances of SimpleCache in the same folder. of `SimpleCache` using the same folder at the same time.
Previous instance must be released.
* Cache redirect URLs * Cache redirect URLs
([#2360](https://github.com/google/ExoPlayer/issues/2360)). ([#2360](https://github.com/google/ExoPlayer/issues/2360)).
* DRM: * DRM:
@ -65,17 +82,41 @@
([#4022][https://github.com/google/ExoPlayer/issues/4022]). ([#4022][https://github.com/google/ExoPlayer/issues/4022]).
* Fix handling of 307/308 redirects when making license requests * Fix handling of 307/308 redirects when making license requests
([#4108](https://github.com/google/ExoPlayer/issues/4108)). ([#4108](https://github.com/google/ExoPlayer/issues/4108)).
* HLS: Fix playlist loading error propagation when the current selection does * HLS:
* Fix playlist loading error propagation when the current selection does
not include all of the playlist's variants. 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 * Fix ClearKey decryption error if the key contains a forward slash
([#4075](https://github.com/google/ExoPlayer/issues/4075)). ([#4075](https://github.com/google/ExoPlayer/issues/4075)).
* Fix crash when switching surface on Huawei P9 Lite * Fix crash when switching surface on Huawei P9 Lite
([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E
([#4104](https://github.com/google/ExoPlayer/issues/4104)). ([#4104](https://github.com/google/ExoPlayer/issues/4104)).
* Support ZLIB compressed PGS subtitles. * 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 * Removed default renderer time offset of 60000000 from internal player. The
actual renderer timestamp offset can be obtained by listening to actual renderer timestamp offset can be obtained by listening to
`BaseRenderer.onStreamChanged`. `BaseRenderer.onStreamChanged`.
* Added dependencies on checkerframework annotations for static code analysis.
### 2.7.3 ### ### 2.7.3 ###
@ -240,6 +281,7 @@
([#3792](https://github.com/google/ExoPlayer/issues/3792). ([#3792](https://github.com/google/ExoPlayer/issues/3792).
* Support 14-bit mode and little endianness in DTS PES packets * Support 14-bit mode and little endianness in DTS PES packets
([#3340](https://github.com/google/ExoPlayer/issues/3340)). ([#3340](https://github.com/google/ExoPlayer/issues/3340)).
* Demo app: Add ability to download not DRM protected content.
### 2.6.1 ### ### 2.6.1 ###

View File

@ -1,6 +1,4 @@
<?xml version="1.0"?> <!-- Copyright (C) 2018 The Android Open Source Project
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,8 +12,8 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources> <lint>
<string name="exo_media_action_repeat_all_description">"Ulang semua"</string> <issue id="InvalidPackage">
<string name="exo_media_action_repeat_off_description">"Tiada ulangan"</string> <ignore path="**/checker-qual-*.jar"/>
<string name="exo_media_action_repeat_one_description">"Ulangan"</string> </issue>
</resources> </lint>

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.7.3' releaseVersion = '2.8.0'
releaseVersionCode = 2703 releaseVersionCode = 2800
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
@ -31,6 +31,8 @@ project.ext {
junitVersion = '4.12' junitVersion = '4.12'
truthVersion = '0.39' truthVersion = '0.39'
robolectricVersion = '3.7.1' robolectricVersion = '3.7.1'
autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix

View File

@ -19,6 +19,8 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-feature android:name="android.software.leanback" android:required="false"/> <uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk/> <uses-sdk/>
@ -73,6 +75,18 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
<service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application> </application>
</manifest> </manifest>

View File

@ -16,6 +16,13 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.ProgressiveDownloadAction;
import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction;
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction;
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
@ -35,10 +42,24 @@ import java.io.File;
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String DOWNLOAD_CACHE_FOLDER = "downloads"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
private static final Deserializer[] DOWNLOAD_DESERIALIZERS =
new Deserializer[] {
DashDownloadAction.DESERIALIZER,
HlsDownloadAction.DESERIALIZER,
SsDownloadAction.DESERIALIZER,
ProgressiveDownloadAction.DESERIALIZER
};
protected String userAgent; protected String userAgent;
private File downloadDirectory;
private Cache downloadCache; private Cache downloadCache;
private DownloadManager downloadManager;
private DownloadTracker downloadTracker;
@Override @Override
public void onCreate() { public void onCreate() {
@ -50,7 +71,7 @@ public class DemoApplication extends Application {
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) { public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
DefaultDataSourceFactory upstreamFactory = DefaultDataSourceFactory upstreamFactory =
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
return createReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
} }
/** Returns a {@link HttpDataSource.Factory}. */ /** Returns a {@link HttpDataSource.Factory}. */
@ -59,31 +80,69 @@ public class DemoApplication extends Application {
return new DefaultHttpDataSourceFactory(userAgent, listener); return new DefaultHttpDataSourceFactory(userAgent, listener);
} }
/** Returns the download {@link Cache}. */ /** Returns whether extension renderers should be used. */
public Cache getDownloadCache() {
if (downloadCache == null) {
File dir = getExternalFilesDir(null);
if (dir == null) {
dir = getFilesDir();
}
File downloadCacheFolder = new File(dir, DOWNLOAD_CACHE_FOLDER);
downloadCache = new SimpleCache(downloadCacheFolder, new NoOpCacheEvictor());
}
return downloadCache;
}
public boolean useExtensionRenderers() { public boolean useExtensionRenderers() {
return "withExtensions".equals(BuildConfig.FLAVOR); return "withExtensions".equals(BuildConfig.FLAVOR);
} }
private static CacheDataSourceFactory createReadOnlyCacheDataSource( 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) { DefaultDataSourceFactory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory( return new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,
new FileDataSourceFactory(), new FileDataSourceFactory(),
/*cacheWriteDataSinkFactory=*/ null, /* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/*eventListener=*/ null); /* 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; package com.google.android.exoplayer2.demo;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Pair;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@ -48,6 +48,7 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
@ -57,14 +58,15 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.dash.manifest.FilteringDashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.source.hls.playlist.RenditionKey;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.FilteringSsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
@ -74,11 +76,12 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.ParcelableArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.net.CookieHandler; import java.net.CookieHandler;
@ -99,13 +102,11 @@ public class PlayerActivity extends Activity
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
public static final String EXTENSION_EXTRA = "extension"; public static final String EXTENSION_EXTRA = "extension";
public static final String MANIFEST_FILTER_EXTRA = "manifest_filter";
public static final String ACTION_VIEW_LIST = public static final String ACTION_VIEW_LIST =
"com.google.android.exoplayer.demo.action.VIEW_LIST"; "com.google.android.exoplayer.demo.action.VIEW_LIST";
public static final String URI_LIST_EXTRA = "uri_list"; public static final String URI_LIST_EXTRA = "uri_list";
public static final String EXTENSION_LIST_EXTRA = "extension_list"; public static final String EXTENSION_LIST_EXTRA = "extension_list";
public static final String MANIFEST_FILTER_LIST_EXTRA = "manifest_filter_list";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
@ -116,6 +117,12 @@ public class PlayerActivity extends Activity
// For backwards compatibility only. // For backwards compatibility only.
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
// Saved instance state keys.
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
private static final String KEY_WINDOW = "window";
private static final String KEY_POSITION = "position";
private static final String KEY_AUTO_PLAY = "auto_play";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER; private static final CookieManager DEFAULT_COOKIE_MANAGER;
static { static {
@ -123,7 +130,6 @@ public class PlayerActivity extends Activity
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
} }
private Handler mainHandler;
private PlayerView playerView; private PlayerView playerView;
private LinearLayout debugRootView; private LinearLayout debugRootView;
private TextView debugTextView; private TextView debugTextView;
@ -132,14 +138,13 @@ public class PlayerActivity extends Activity
private SimpleExoPlayer player; private SimpleExoPlayer player;
private MediaSource mediaSource; private MediaSource mediaSource;
private DefaultTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper; private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
private boolean inErrorState;
private TrackGroupArray lastSeenTrackGroupArray; private TrackGroupArray lastSeenTrackGroupArray;
private boolean shouldAutoPlay; private boolean startAutoPlay;
private int resumeWindow; private int startWindow;
private long resumePosition; private long startPosition;
// Fields used only for ad playback. The ads loader is loaded via reflection. // Fields used only for ad playback. The ads loader is loaded via reflection.
@ -152,10 +157,7 @@ public class PlayerActivity extends Activity
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
shouldAutoPlay = true;
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true); mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
@ -168,14 +170,24 @@ public class PlayerActivity extends Activity
playerView = findViewById(R.id.player_view); playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this); playerView.setControllerVisibilityListener(this);
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
playerView.requestFocus(); playerView.requestFocus();
if (savedInstanceState != null) {
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
startWindow = savedInstanceState.getInt(KEY_WINDOW);
startPosition = savedInstanceState.getLong(KEY_POSITION);
} else {
trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
clearStartPosition();
}
} }
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
releasePlayer(); releasePlayer();
shouldAutoPlay = true; clearStartPosition();
clearResumePosition();
setIntent(intent); setIntent(intent);
} }
@ -220,17 +232,27 @@ public class PlayerActivity extends Activity
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) { @NonNull int[] grantResults) {
if (grantResults.length > 0) { 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) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initializePlayer(); initializePlayer();
} else { } else {
showToast(R.string.storage_permission_denied); showToast(R.string.storage_permission_denied);
finish(); finish();
} }
} else {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
} }
@Override
public void onSaveInstanceState(Bundle outState) {
updateTrackSelectorParameters();
updateStartPosition();
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
outState.putInt(KEY_WINDOW, startWindow);
outState.putLong(KEY_POSITION, startPosition);
} }
// Activity input // Activity input
@ -248,8 +270,19 @@ public class PlayerActivity extends Activity
if (view.getParent() == debugRootView) { if (view.getParent() == debugRootView) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) { if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog( CharSequence title = ((Button) view).getText();
this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag()); int rendererIndex = (int) view.getTag();
int rendererType = mappedTrackInfo.getRendererType(rendererIndex);
boolean allowAdaptiveSelections =
rendererType == C.TRACK_TYPE_VIDEO
|| (rendererType == C.TRACK_TYPE_AUDIO
&& mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS);
Pair<AlertDialog, TrackSelectionView> dialogPair =
TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex);
dialogPair.second.setShowDisableOption(true);
dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections);
dialogPair.first.show();
} }
} }
} }
@ -276,11 +309,9 @@ public class PlayerActivity extends Activity
String action = intent.getAction(); String action = intent.getAction();
Uri[] uris; Uri[] uris;
String[] extensions; String[] extensions;
Parcelable[] manifestFilters;
if (ACTION_VIEW.equals(action)) { if (ACTION_VIEW.equals(action)) {
uris = new Uri[] {intent.getData()}; uris = new Uri[] {intent.getData()};
extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
manifestFilters = new Parcelable[] {intent.getParcelableExtra(MANIFEST_FILTER_EXTRA)};
} else if (ACTION_VIEW_LIST.equals(action)) { } else if (ACTION_VIEW_LIST.equals(action)) {
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
uris = new Uri[uriStrings.length]; uris = new Uri[uriStrings.length];
@ -291,10 +322,6 @@ public class PlayerActivity extends Activity
if (extensions == null) { if (extensions == null) {
extensions = new String[uriStrings.length]; extensions = new String[uriStrings.length];
} }
manifestFilters = intent.getParcelableArrayExtra(MANIFEST_FILTER_LIST_EXTRA);
if (manifestFilters == null) {
manifestFilters = new Parcelable[uriStrings.length];
}
} else { } else {
showToast(getString(R.string.unexpected_intent_action, action)); showToast(getString(R.string.unexpected_intent_action, action));
finish(); finish();
@ -361,23 +388,14 @@ public class PlayerActivity extends Activity
new DefaultRenderersFactory(this, extensionRendererMode); new DefaultRenderersFactory(this, extensionRendererMode);
trackSelector = new DefaultTrackSelector(trackSelectionFactory); trackSelector = new DefaultTrackSelector(trackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, trackSelectionFactory); trackSelector.setParameters(trackSelectorParameters);
lastSeenTrackGroupArray = null; lastSeenTrackGroupArray = null;
player = player =
ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager); ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager);
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
EventLogger eventLogger = new EventLogger(trackSelector); player.addAnalyticsListener(new EventLogger(trackSelector));
player.addListener(eventLogger);
player.addMetadataOutput(eventLogger);
player.addAudioDebugListener(eventLogger);
player.addVideoDebugListener(eventLogger);
if (drmSessionManager != null) {
drmSessionManager.addListener(mainHandler, eventLogger);
}
player.setPlayWhenReady(shouldAutoPlay);
playerView.setPlayer(player); playerView.setPlayer(player);
playerView.setPlaybackPreparer(this); playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper = new DebugTextViewHelper(player, debugTextView);
@ -385,9 +403,7 @@ public class PlayerActivity extends Activity
MediaSource[] mediaSources = new MediaSource[uris.length]; MediaSource[] mediaSources = new MediaSource[uris.length];
for (int i = 0; i < uris.length; i++) { for (int i = 0; i < uris.length; i++) {
ParcelableArray<?> manifestFilter = (ParcelableArray<?>) manifestFilters[i]; mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
List<?> filter = manifestFilter != null ? manifestFilter.asList() : null;
mediaSources[i] = buildMediaSource(uris[i], extensions[i], filter);
} }
mediaSource = mediaSource =
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
@ -398,8 +414,7 @@ public class PlayerActivity extends Activity
releaseAdsLoader(); releaseAdsLoader();
loadedAdTagUri = adTagUri; loadedAdTagUri = adTagUri;
} }
MediaSource adsMediaSource = MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
createAdsMediaSource(mediaSource, Uri.parse(adTagUriString), eventLogger);
if (adsMediaSource != null) { if (adsMediaSource != null) {
mediaSource = adsMediaSource; mediaSource = adsMediaSource;
} else { } else {
@ -408,24 +423,21 @@ public class PlayerActivity extends Activity
} else { } else {
releaseAdsLoader(); releaseAdsLoader();
} }
mediaSource.addEventListener(mainHandler, eventLogger);
} }
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; boolean haveStartPosition = startWindow != C.INDEX_UNSET;
if (haveResumePosition) { if (haveStartPosition) {
player.seekTo(resumeWindow, resumePosition); player.seekTo(startWindow, startPosition);
} }
player.prepare(mediaSource, !haveResumePosition, false); player.prepare(mediaSource, !haveStartPosition, false);
inErrorState = false;
updateButtonVisibilities(); updateButtonVisibilities();
} }
private MediaSource buildMediaSource(Uri uri) { private MediaSource buildMediaSource(Uri uri) {
return buildMediaSource(uri, null, null); return buildMediaSource(uri, null);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private MediaSource buildMediaSource( private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
Uri uri, @Nullable String overrideExtension, @Nullable List<?> manifestFilter) {
@ContentType int type = Util.inferContentType(uri, overrideExtension); @ContentType int type = Util.inferContentType(uri, overrideExtension);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
@ -433,17 +445,22 @@ public class PlayerActivity extends Activity
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)) buildDataSourceFactory(false))
.setManifestParser( .setManifestParser(
new FilteringDashManifestParser((List<RepresentationKey>) manifestFilter)) new FilteringManifestParser<>(
new DashManifestParser(), (List<RepresentationKey>) getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource.Factory( return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)) buildDataSourceFactory(false))
.setManifestParser(new FilteringSsManifestParser((List<TrackKey>) manifestFilter)) .setManifestParser(
new FilteringManifestParser<>(
new SsManifestParser(), (List<StreamKey>) getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory) return new HlsMediaSource.Factory(mediaDataSourceFactory)
.setPlaylistParser(new FilteringHlsPlaylistParser((List<String>) manifestFilter)) .setPlaylistParser(
new FilteringManifestParser<>(
new HlsPlaylistParser(), (List<RenditionKey>) getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
@ -453,45 +470,58 @@ public class PlayerActivity extends Activity
} }
} }
private List<?> getOfflineStreamKeys(Uri uri) {
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
}
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18( private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
throws UnsupportedDrmException { throws UnsupportedDrmException {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpDataSource.Factory licenseDataSourceFactory =
buildHttpDataSourceFactory(false)); ((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
if (keyRequestPropertiesArray != null) { if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]); keyRequestPropertiesArray[i + 1]);
} }
} }
DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = return new DefaultDrmSessionManager<>(
new DefaultDrmSessionManager<>(
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession); uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
return drmSessionManager;
} }
private void releasePlayer() { private void releasePlayer() {
if (player != null) { if (player != null) {
updateTrackSelectorParameters();
updateStartPosition();
debugViewHelper.stop(); debugViewHelper.stop();
debugViewHelper = null; debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady();
updateResumePosition();
player.release(); player.release();
player = null; player = null;
mediaSource = null; mediaSource = null;
trackSelector = null; trackSelector = null;
trackSelectionHelper = null;
} }
} }
private void updateResumePosition() { private void updateTrackSelectorParameters() {
resumeWindow = player.getCurrentWindowIndex(); if (trackSelector != null) {
resumePosition = Math.max(0, player.getContentPosition()); trackSelectorParameters = trackSelector.getParameters();
}
} }
private void clearResumePosition() { private void updateStartPosition() {
resumeWindow = C.INDEX_UNSET; if (player != null) {
resumePosition = C.TIME_UNSET; startAutoPlay = player.getPlayWhenReady();
startWindow = player.getCurrentWindowIndex();
startPosition = Math.max(0, player.getContentPosition());
}
}
private void clearStartPosition() {
startAutoPlay = true;
startWindow = C.INDEX_UNSET;
startPosition = C.TIME_UNSET;
} }
/** /**
@ -506,21 +536,8 @@ public class PlayerActivity extends Activity
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); .buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
} }
/**
* Returns a new HttpDataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
* DataSource factory.
* @return A new HttpDataSource factory.
*/
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
return ((DemoApplication) getApplication())
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
/** Returns an ads media source, reusing the ads loader if one exists. */ /** Returns an ads media source, reusing the ads loader if one exists. */
private @Nullable MediaSource createAdsMediaSource( private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
MediaSource mediaSource, Uri adTagUri, EventLogger eventLogger) {
// Load the extension source using reflection so the demo app doesn't have to depend on it. // Load the extension source using reflection so the demo app doesn't have to depend on it.
// The ads loader is reused for multiple playbacks, so that ad playback can resume. // The ads loader is reused for multiple playbacks, so that ad playback can resume.
try { try {
@ -550,8 +567,7 @@ public class PlayerActivity extends Activity
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
} }
}; };
return new AdsMediaSource( return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup);
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
// IMA extension not loaded. // IMA extension not loaded.
return null; return null;
@ -582,20 +598,20 @@ public class PlayerActivity extends Activity
return; return;
} }
for (int i = 0; i < mappedTrackInfo.length; i++) { for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i); TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
if (trackGroups.length != 0) { if (trackGroups.length != 0) {
Button button = new Button(this); Button button = new Button(this);
int label; int label;
switch (player.getRendererType(i)) { switch (player.getRendererType(i)) {
case C.TRACK_TYPE_AUDIO: case C.TRACK_TYPE_AUDIO:
label = R.string.audio; label = R.string.exo_track_selection_title_audio;
break; break;
case C.TRACK_TYPE_VIDEO: case C.TRACK_TYPE_VIDEO:
label = R.string.video; label = R.string.exo_track_selection_title_video;
break; break;
case C.TRACK_TYPE_TEXT: case C.TRACK_TYPE_TEXT:
label = R.string.text; label = R.string.exo_track_selection_title_text;
break; break;
default: default:
continue; continue;
@ -646,48 +662,20 @@ public class PlayerActivity extends Activity
@Override @Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (inErrorState) { if (player.getPlaybackError() != null) {
// This will only occur if the user has performed a seek whilst in the error state. Update // The user has performed a seek whilst in the error state. Update the resume position so
// the resume position so that if the user then retries, playback will resume from the // that if the user then retries, playback resumes from the position to which they seeked.
// position to which they seeked. updateStartPosition();
updateResumePosition();
} }
} }
@Override @Override
public void onPlayerError(ExoPlaybackException e) { public void onPlayerError(ExoPlaybackException e) {
String errorString = null;
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof DecoderInitializationException) {
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
if (decoderInitializationException.decoderName == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString = getString(R.string.error_no_secure_decoder,
decoderInitializationException.mimeType);
} else {
errorString = getString(R.string.error_no_decoder,
decoderInitializationException.mimeType);
}
} else {
errorString = getString(R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
}
}
}
if (errorString != null) {
showToast(errorString);
}
inErrorState = true;
if (isBehindLiveWindow(e)) { if (isBehindLiveWindow(e)) {
clearResumePosition(); clearStartPosition();
initializePlayer(); initializePlayer();
} else { } else {
updateResumePosition(); updateStartPosition();
updateButtonVisibilities(); updateButtonVisibilities();
showControls(); showControls();
} }
@ -700,11 +688,11 @@ public class PlayerActivity extends Activity
if (trackGroups != lastSeenTrackGroupArray) { if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) { if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO) if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video); showToast(R.string.error_unsupported_video);
} }
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO) if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio); showToast(R.string.error_unsupported_audio);
} }
@ -712,7 +700,40 @@ public class PlayerActivity extends Activity
lastSeenTrackGroupArray = trackGroups; lastSeenTrackGroupArray = trackGroups;
} }
} }
}
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
@Override
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
String errorString = getString(R.string.error_generic);
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof DecoderInitializationException) {
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
if (decoderInitializationException.decoderName == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString =
getString(
R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
} else {
errorString =
getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
}
} else {
errorString =
getString(
R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
}
}
}
return Pair.create(0, errorString);
}
} }
} }

View File

@ -24,15 +24,17 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter; import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView; import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -44,19 +46,27 @@ import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /** An activity for selecting from a list of media samples. */
* An activity for selecting from a list of samples. public class SampleChooserActivity extends Activity
*/ implements DownloadTracker.Listener, OnChildClickListener {
public class SampleChooserActivity extends Activity {
private static final String TAG = "SampleChooserActivity"; private static final String TAG = "SampleChooserActivity";
private DownloadTracker downloadTracker;
private SampleAdapter sampleAdapter;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.sample_chooser_activity); setContentView(R.layout.sample_chooser_activity);
sampleAdapter = new SampleAdapter();
ExpandableListView sampleListView = findViewById(R.id.sample_list);
sampleListView.setAdapter(sampleAdapter);
sampleListView.setOnChildClickListener(this);
Intent intent = getIntent(); Intent intent = getIntent();
String dataUri = intent.getDataString(); String dataUri = intent.getDataString();
String[] uris; String[] uris;
@ -79,8 +89,32 @@ public class SampleChooserActivity extends Activity {
uriList.toArray(uris); uriList.toArray(uris);
Arrays.sort(uris); Arrays.sort(uris);
} }
downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker();
SampleListLoader loaderTask = new SampleListLoader(); SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris); loaderTask.execute(uris);
// Ping the download service in case it's not running (but should be).
startService(
new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
}
@Override
public void onStart() {
super.onStart();
downloadTracker.addListener(this);
sampleAdapter.notifyDataSetChanged();
}
@Override
public void onStop() {
downloadTracker.removeListener(this);
super.onStop();
}
@Override
public void onDownloadsChanged() {
sampleAdapter.notifyDataSetChanged();
} }
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) { private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
@ -88,20 +122,44 @@ public class SampleChooserActivity extends Activity {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show(); .show();
} }
ExpandableListView sampleList = findViewById(R.id.sample_list); sampleAdapter.setSampleGroups(groups);
sampleList.setAdapter(new SampleAdapter(this, groups));
sampleList.setOnChildClickListener(new OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
int childPosition, long id) {
onSampleSelected(groups.get(groupPosition).samples.get(childPosition));
return true;
}
});
} }
private void onSampleSelected(Sample sample) { @Override
public boolean onChildClick(
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
Sample sample = (Sample) view.getTag();
startActivity(sample.buildIntent(this)); startActivity(sample.buildIntent(this));
return true;
}
private void onSampleDownloadButtonClicked(Sample sample) {
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
if (downloadUnsupportedStringId != 0) {
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
.show();
} else {
UriSample uriSample = (UriSample) sample;
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
}
}
private int getDownloadUnsupportedStringId(Sample sample) {
if (sample instanceof PlaylistSample) {
return R.string.download_playlist_unsupported;
}
UriSample uriSample = (UriSample) sample;
if (uriSample.drmInfo != null) {
return R.string.download_drm_unsupported;
}
if (uriSample.adTagUri != null) {
return R.string.download_ads_unsupported;
}
String scheme = uriSample.uri.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) {
return R.string.download_scheme_unsupported;
}
return 0;
} }
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> { private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
@ -175,7 +233,7 @@ public class SampleChooserActivity extends Activity {
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
String sampleName = null; String sampleName = null;
String uri = null; Uri uri = null;
String extension = null; String extension = null;
String drmScheme = null; String drmScheme = null;
String drmLicenseUrl = null; String drmLicenseUrl = null;
@ -194,7 +252,7 @@ public class SampleChooserActivity extends Activity {
sampleName = reader.nextString(); sampleName = reader.nextString();
break; break;
case "uri": case "uri":
uri = reader.nextString(); uri = Uri.parse(reader.nextString());
break; break;
case "extension": case "extension":
extension = reader.nextString(); extension = reader.nextString();
@ -278,14 +336,17 @@ public class SampleChooserActivity extends Activity {
} }
private static final class SampleAdapter extends BaseExpandableListAdapter { private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
private final Context context; private List<SampleGroup> sampleGroups;
private final List<SampleGroup> sampleGroups;
public SampleAdapter(Context context, List<SampleGroup> sampleGroups) { public SampleAdapter() {
this.context = context; sampleGroups = Collections.emptyList();
}
public void setSampleGroups(List<SampleGroup> sampleGroups) {
this.sampleGroups = sampleGroups; this.sampleGroups = sampleGroups;
notifyDataSetChanged();
} }
@Override @Override
@ -303,10 +364,12 @@ public class SampleChooserActivity extends Activity {
View convertView, ViewGroup parent) { View convertView, ViewGroup parent) {
View view = convertView; View view = convertView;
if (view == null) { if (view == null) {
view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
false); View downloadButton = view.findViewById(R.id.download_button);
downloadButton.setOnClickListener(this);
downloadButton.setFocusable(false);
} }
((TextView) view).setText(getChild(groupPosition, childPosition).name); initializeChildView(view, getChild(groupPosition, childPosition));
return view; return view;
} }
@ -330,8 +393,9 @@ public class SampleChooserActivity extends Activity {
ViewGroup parent) { ViewGroup parent) {
View view = convertView; View view = convertView;
if (view == null) { if (view == null) {
view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1, view =
parent, false); getLayoutInflater()
.inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
} }
((TextView) view).setText(getGroup(groupPosition).title); ((TextView) view).setText(getGroup(groupPosition).title);
return view; return view;
@ -352,6 +416,25 @@ public class SampleChooserActivity extends Activity {
return true; return true;
} }
@Override
public void onClick(View view) {
onSampleDownloadButtonClicked((Sample) view.getTag());
}
private void initializeChildView(View view, Sample sample) {
view.setTag(sample);
TextView sampleTitle = view.findViewById(R.id.sample_title);
sampleTitle.setText(sample.name);
boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
ImageButton downloadButton = view.findViewById(R.id.download_button);
downloadButton.setTag(sample);
downloadButton.setColorFilter(
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE);
downloadButton.setImageResource(
isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
}
} }
private static final class SampleGroup { private static final class SampleGroup {
@ -420,7 +503,7 @@ public class SampleChooserActivity extends Activity {
private static final class UriSample extends Sample { private static final class UriSample extends Sample {
public final String uri; public final Uri uri;
public final String extension; public final String extension;
public final String adTagUri; public final String adTagUri;
@ -429,7 +512,7 @@ public class SampleChooserActivity extends Activity {
boolean preferExtensionDecoders, boolean preferExtensionDecoders,
String abrAlgorithm, String abrAlgorithm,
DrmInfo drmInfo, DrmInfo drmInfo,
String uri, Uri uri,
String extension, String extension,
String adTagUri) { String adTagUri) {
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
@ -441,7 +524,7 @@ public class SampleChooserActivity extends Activity {
@Override @Override
public Intent buildIntent(Context context) { public Intent buildIntent(Context context) {
return super.buildIntent(context) return super.buildIntent(context)
.setData(Uri.parse(uri)) .setData(uri)
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.setAction(PlayerActivity.ACTION_VIEW); .setAction(PlayerActivity.ACTION_VIEW);
@ -468,7 +551,7 @@ public class SampleChooserActivity extends Activity {
String[] uris = new String[children.length]; String[] uris = new String[children.length];
String[] extensions = new String[children.length]; String[] extensions = new String[children.length];
for (int i = 0; i < children.length; i++) { for (int i = 0; i < children.length; i++) {
uris[i] = children[i].uri; uris[i] = children[i].uri.toString();
extensions[i] = children[i].extension; extensions[i] = children[i].extension;
} }
return super.buildIntent(context) return super.buildIntent(context)

View File

@ -1,255 +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.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
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 final DefaultTrackSelector selector;
private final TrackSelection.Factory trackSelectionFactory;
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[][] trackViews;
/**
* @param selector The track selector.
* @param trackSelectionFactory A factory for overriding {@link TrackSelection}s.
*/
public TrackSelectionHelper(
DefaultTrackSelector selector, TrackSelection.Factory trackSelectionFactory) {
this.selector = selector;
this.trackSelectionFactory = trackSelectionFactory;
}
/**
* 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] =
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.
trackViews = new CheckedTextView[trackGroups.length][];
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
TrackGroup group = trackGroups.get(groupIndex);
boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex];
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);
}
}
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));
}
}
}
// 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 {
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) {
setOverride(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));
}
} else {
// Add the track to the override.
setOverride(groupIndex, getTracksAdding(override, trackIndex));
}
}
}
// Update the views with the new state.
updateViews();
}
private void setOverride(int group, int... tracks) {
override = new SelectionOverride(trackSelectionFactory, 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;
}
}

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"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!-- Copyright (C) 2018 The Android Open Source Project
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,8 +13,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources> <ListView xmlns:android="http://schemas.android.com/apk/res/android"
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string> android:id="@+id/representation_list"
<string name="exo_media_action_repeat_off_description">"Non repetir"</string> android:layout_width="match_parent"
<string name="exo_media_action_repeat_one_description">"Repetir un"</string> android:layout_height="match_parent"/>
</resources>

View File

@ -17,18 +17,10 @@
<string name="application_name">ExoPlayer</string> <string name="application_name">ExoPlayer</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="text">Text</string>
<string name="selection_disabled">Disabled</string>
<string name="selection_default">Default</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string> <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string> <string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string> <string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
@ -55,4 +47,14 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string>
</resources> </resources>

View File

@ -19,6 +19,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
@ -307,6 +308,11 @@ public final class CastPlayer implements Player {
return playbackState; return playbackState;
} }
@Override
public ExoPlaybackException getPlaybackError() {
return null;
}
@Override @Override
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {

View File

@ -280,6 +280,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest)); new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID); throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
} }
@ -352,17 +353,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
if (!operation.block(readTimeoutMs)) { if (!operation.block(readTimeoutMs)) {
throw new SocketTimeoutException(); throw new SocketTimeoutException();
} }
} catch (InterruptedException | SocketTimeoutException e) { } catch (InterruptedException e) {
// If we're timing out or getting interrupted, the operation is still ongoing. // The operation is ongoing so replace readBuffer to avoid it being written to by this
// So we'll need to replace readBuffer to avoid the possibility of it being written to by // operation during a subsequent request.
// this operation during a subsequent request.
readBuffer = null; readBuffer = null;
Thread.currentThread().interrupt();
throw new HttpDataSourceException( throw new HttpDataSourceException(
e instanceof InterruptedException new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
? new InterruptedIOException((InterruptedException) e) } catch (SocketTimeoutException e) {
: (SocketTimeoutException) e, // The operation is ongoing so replace readBuffer to avoid it being written to by this
currentDataSpec, // operation during a subsequent request.
HttpDataSourceException.TYPE_READ); readBuffer = null;
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
} }
if (exception != null) { if (exception != null) {

View File

@ -21,6 +21,7 @@ import android.util.Log;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@ -86,7 +87,7 @@ public final class CronetEngineWrapper {
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) { public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null; CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE; @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context); List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
// Remove disabled and fallback Cronet providers from list // Remove disabled and fallback Cronet providers from list
for (int i = cronetProviders.size() - 1; i >= 0; i--) { for (int i = cronetProviders.size() - 1; i >= 0; i--) {
if (!cronetProviders.get(i).isEnabled() if (!cronetProviders.get(i).isEnabled()

View File

@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
*/ */
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioSink audioSink, boolean enableFloatOutput) { AudioSink audioSink, boolean enableFloatOutput) {
super(eventHandler, eventListener, null, false, audioSink); super(
eventHandler,
eventListener,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
audioSink);
this.enableFloatOutput = enableFloatOutput; this.enableFloatOutput = enableFloatOutput;
} }

View File

@ -34,6 +34,7 @@ dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
ext { ext {

View File

@ -79,7 +79,7 @@ public final class FlacExtractor implements Extractor {
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private final Id3Peeker id3Peeker; private final Id3Peeker id3Peeker;
private final @Flags int flags; private final boolean isId3MetadataDisabled;
private FlacDecoderJni decoderJni; private FlacDecoderJni decoderJni;
@ -90,7 +90,6 @@ public final class FlacExtractor implements Extractor {
private ByteBuffer outputByteBuffer; private ByteBuffer outputByteBuffer;
private Metadata id3Metadata; private Metadata id3Metadata;
private long id3SectionSize;
private boolean metadataParsed; private boolean metadataParsed;
@ -105,8 +104,8 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior. * @param flags Flags that control the extractor's behavior.
*/ */
public FlacExtractor(int flags) { public FlacExtractor(int flags) {
this.flags = flags;
id3Peeker = new Id3Peeker(); id3Peeker = new Id3Peeker();
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
} }
@Override @Override
@ -125,24 +124,16 @@ public final class FlacExtractor implements Extractor {
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
if (input.getPosition() == 0) { if (input.getPosition() == 0) {
id3Metadata = peekId3Data(input); id3Metadata = peekId3Data(input);
id3SectionSize = input.getPeekPosition();
} }
boolean isFlacFormat = peekFlacSignature(input); return peekFlacSignature(input);
if (isFlacFormat) {
// If this is FLAC format, we should skip the whole ID3 section.
skipFullyId3Section(input);
}
return isFlacFormat;
} }
@Override @Override
public int read(final ExtractorInput input, PositionHolder seekPosition) public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() == 0) { if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input); id3Metadata = peekId3Data(input);
id3SectionSize = input.getPeekPosition();
} }
skipFullyId3Section(input);
decoderJni.setData(input); decoderJni.setData(input);
@ -155,7 +146,7 @@ public final class FlacExtractor implements Extractor {
} }
} catch (IOException e) { } catch (IOException e) {
decoderJni.reset(0); decoderJni.reset(0);
input.setRetryPosition(id3SectionSize, e); input.setRetryPosition(0, e);
throw e; // never executes throw e; // never executes
} }
metadataParsed = true; metadataParsed = true;
@ -163,7 +154,7 @@ public final class FlacExtractor implements Extractor {
boolean isSeekable = decoderJni.getSeekPosition(0) != -1; boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
extractorOutput.seekMap( extractorOutput.seekMap(
isSeekable isSeekable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni, id3SectionSize) ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: new SeekMap.Unseekable(streamInfo.durationUs(), 0)); : new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat = Format mediaFormat =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
@ -181,7 +172,7 @@ public final class FlacExtractor implements Extractor {
/* drmInitData= */ null, /* drmInitData= */ null,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null, /* language= */ null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : id3Metadata); isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat); trackOutput.format(mediaFormat);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
@ -196,7 +187,7 @@ public final class FlacExtractor implements Extractor {
} catch (IOException e) { } catch (IOException e) {
if (lastDecodePosition >= 0) { if (lastDecodePosition >= 0) {
decoderJni.reset(lastDecodePosition); decoderJni.reset(lastDecodePosition);
input.setRetryPosition(id3SectionSize + lastDecodePosition, e); input.setRetryPosition(lastDecodePosition, e);
} }
throw e; throw e;
} }
@ -212,12 +203,11 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position <= id3SectionSize) { if (position == 0) {
metadataParsed = false; metadataParsed = false;
} }
long flacStreamPosition = Math.max(0, position - id3SectionSize);
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.reset(flacStreamPosition); decoderJni.reset(position);
} }
} }
@ -238,9 +228,8 @@ public final class FlacExtractor implements Extractor {
@Nullable @Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition(); input.resetPeekPosition();
boolean disableId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
Id3Decoder.FramePredicate id3FramePredicate = Id3Decoder.FramePredicate id3FramePredicate =
disableId3Frames ? Id3Decoder.NO_FRAMES_PREDICATE : null; isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate); return id3Peeker.peekId3Data(input, id3FramePredicate);
} }
@ -255,22 +244,14 @@ public final class FlacExtractor implements Extractor {
return Arrays.equals(header, FLAC_SIGNATURE); return Arrays.equals(header, FLAC_SIGNATURE);
} }
/** Skips input until we have passed the whole Id3 section. */
private void skipFullyId3Section(ExtractorInput input) throws IOException, InterruptedException {
int bytesToSkip = Math.max(0, (int) (id3SectionSize - input.getPosition()));
input.skipFully(bytesToSkip);
}
private static final class FlacSeekMap implements SeekMap { private static final class FlacSeekMap implements SeekMap {
private final long durationUs; private final long durationUs;
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
private final long id3SectionSize;
public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni, long id3SectionSize) { public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
this.durationUs = durationUs; this.durationUs = durationUs;
this.decoderJni = decoderJni; this.decoderJni = decoderJni;
this.id3SectionSize = id3SectionSize;
} }
@Override @Override
@ -281,8 +262,7 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public SeekPoints getSeekPoints(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
// TODO: Access the seek table via JNI to return two seek points when appropriate. // TODO: Access the seek table via JNI to return two seek points when appropriate.
return new SeekPoints( return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
new SeekPoint(timeUs, id3SectionSize + decoderJni.getSeekPosition(timeUs)));
} }
@Override @Override

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;
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -80,6 +82,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private final Context context; private final Context context;
private @Nullable ImaSdkSettings imaSdkSettings; private @Nullable ImaSdkSettings imaSdkSettings;
private @Nullable AdEventListener adEventListener;
private int vastLoadTimeoutMs; private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs; private int mediaLoadTimeoutMs;
@ -108,6 +111,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return this; return this;
} }
/**
* Sets a listener for ad events that will be passed to {@link
* AdsManager#addAdEventListener(AdEventListener)}.
*
* @param adEventListener The ad event listener.
* @return This builder, for convenience.
*/
public Builder setAdEventListener(AdEventListener adEventListener) {
this.adEventListener = Assertions.checkNotNull(adEventListener);
return this;
}
/** /**
* Sets the VAST load timeout, in milliseconds. * Sets the VAST load timeout, in milliseconds.
* *
@ -144,7 +159,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/ */
public ImaAdsLoader buildForAdTag(Uri adTagUri) { public ImaAdsLoader buildForAdTag(Uri adTagUri) {
return new ImaAdsLoader( return new ImaAdsLoader(
context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs, mediaLoadTimeoutMs); context,
adTagUri,
imaSdkSettings,
null,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
adEventListener);
} }
/** /**
@ -156,7 +177,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/ */
public ImaAdsLoader buildForAdsResponse(String adsResponse) { public ImaAdsLoader buildForAdsResponse(String adsResponse) {
return new ImaAdsLoader( return new ImaAdsLoader(
context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs, mediaLoadTimeoutMs); context,
null,
imaSdkSettings,
adsResponse,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
adEventListener);
} }
} }
@ -214,6 +241,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private final @Nullable String adsResponse; private final @Nullable String adsResponse;
private final int vastLoadTimeoutMs; private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs; private final int mediaLoadTimeoutMs;
private final @Nullable AdEventListener adEventListener;
private final Timeline.Period period; private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayerCallback> adCallbacks;
private final ImaSdkFactory imaSdkFactory; private final ImaSdkFactory imaSdkFactory;
@ -229,7 +257,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private VideoProgressUpdate lastAdProgress; private VideoProgressUpdate lastAdProgress;
private AdsManager adsManager; private AdsManager adsManager;
private AdErrorEvent pendingAdErrorEvent; private AdLoadException pendingAdLoadError;
private Timeline timeline; private Timeline timeline;
private long contentDurationMs; private long contentDurationMs;
private int podIndexOffset; private int podIndexOffset;
@ -308,7 +336,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/* imaSdkSettings= */ null, /* imaSdkSettings= */ null,
/* adsResponse= */ null, /* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET); /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
/* adEventListener= */ null);
} }
/** /**
@ -330,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
imaSdkSettings, imaSdkSettings,
/* adsResponse= */ null, /* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET); /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
/* adEventListener= */ null);
} }
private ImaAdsLoader( private ImaAdsLoader(
@ -339,12 +369,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Nullable ImaSdkSettings imaSdkSettings, @Nullable ImaSdkSettings imaSdkSettings,
@Nullable String adsResponse, @Nullable String adsResponse,
int vastLoadTimeoutMs, int vastLoadTimeoutMs,
int mediaLoadTimeoutMs) { int mediaLoadTimeoutMs,
@Nullable AdEventListener adEventListener) {
Assertions.checkArgument(adTagUri != null || adsResponse != null); Assertions.checkArgument(adTagUri != null || adsResponse != null);
this.adTagUri = adTagUri; this.adTagUri = adTagUri;
this.adsResponse = adsResponse; this.adsResponse = adsResponse;
this.vastLoadTimeoutMs = vastLoadTimeoutMs; this.vastLoadTimeoutMs = vastLoadTimeoutMs;
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
this.adEventListener = adEventListener;
period = new Timeline.Period(); period = new Timeline.Period();
adCallbacks = new ArrayList<>(1); adCallbacks = new ArrayList<>(1);
imaSdkFactory = ImaSdkFactory.getInstance(); imaSdkFactory = ImaSdkFactory.getInstance();
@ -500,6 +532,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
this.adsManager = adsManager; this.adsManager = adsManager;
adsManager.addAdErrorListener(this); adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this); adsManager.addAdEventListener(this);
if (adEventListener != null) {
adsManager.addAdEventListener(adEventListener);
}
if (player != null) { if (player != null) {
// If a player is attached already, start playback immediately. // If a player is attached already, start playback immediately.
try { try {
@ -544,13 +579,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
updateAdPlaybackState(); updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) { } else if (isAdGroupLoadError(error)) {
try { try {
handleAdGroupLoadError(); handleAdGroupLoadError(error);
} catch (Exception e) { } catch (Exception e) {
maybeNotifyInternalError("onAdError", e); maybeNotifyInternalError("onAdError", e);
} }
} }
if (pendingAdErrorEvent == null) { if (pendingAdLoadError == null) {
pendingAdErrorEvent = adErrorEvent; pendingAdLoadError = AdLoadException.createForAllAds(error);
} }
maybeNotifyPendingAdLoadError(); maybeNotifyPendingAdLoadError();
} }
@ -937,9 +972,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
break; break;
case LOG: case LOG:
Map<String, String> adData = adEvent.getAdData(); Map<String, String> adData = adEvent.getAdData();
Log.i(TAG, "Log AdEvent: " + adData); String message = "AdEvent: " + adData;
Log.i(TAG, message);
if ("adLoadError".equals(adData.get("type"))) { if ("adLoadError".equals(adData.get("type"))) {
handleAdGroupLoadError(); handleAdGroupLoadError(new IOException(message));
} }
break; break;
case ALL_ADS_COMPLETED: case ALL_ADS_COMPLETED:
@ -1011,7 +1047,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
} }
private void handleAdGroupLoadError() { private void handleAdGroupLoadError(Exception error) {
int adGroupIndex = int adGroupIndex =
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
@ -1033,6 +1069,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
} }
updateAdPlaybackState(); updateAdPlaybackState();
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
}
} }
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
@ -1111,21 +1150,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
private void maybeNotifyPendingAdLoadError() { private void maybeNotifyPendingAdLoadError() {
if (pendingAdErrorEvent != null) { if (pendingAdLoadError != null && eventListener != null) {
if (eventListener != null) { eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri));
eventListener.onAdLoadError( pendingAdLoadError = null;
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
}
pendingAdErrorEvent = null;
} }
} }
private void maybeNotifyInternalError(String name, Exception cause) { private void maybeNotifyInternalError(String name, Exception cause) {
String message = "Internal error in " + name; String message = "Internal error in " + name;
Log.e(TAG, message, cause); Log.e(TAG, message, cause);
if (eventListener != null) {
eventListener.onInternalAdLoadError(new RuntimeException(message, cause));
}
// We can't recover from an unexpected error in general, so skip all remaining ads. // We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) { if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState(); adPlaybackState = new AdPlaybackState();
@ -1135,6 +1168,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
} }
updateAdPlaybackState(); updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
new DataSpec(adTagUri));
}
} }
private static long[] getAdGroupTimesUs(List<Float> cuePoints) { private static long[] getAdGroupTimesUs(List<Float> cuePoints) {

View File

@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer2.ext.jobdispatcher; package com.google.android.exoplayer2.ext.jobdispatcher;
import android.app.Notification;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@ -34,13 +32,8 @@ import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
* A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
* schedule a {@link Service} to be started when its requirements are met. The started service must * {@link JobDispatcherSchedulerService} to your manifest:
* call {@link Service#startForeground(int, Notification)} to make itself a foreground service upon
* being started, as documented by {@link Service#startForegroundService(Intent)}.
*
* <p>To use {@link JobDispatcherScheduler} application needs to have RECEIVE_BOOT_COMPLETED
* permission and you need to define JobDispatcherSchedulerService in your manifest:
* *
* <pre>{@literal * <pre>{@literal
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> * <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
@ -54,18 +47,6 @@ import com.google.android.exoplayer2.util.Util;
* </service> * </service>
* }</pre> * }</pre>
* *
* The service to be scheduled must be defined in the manifest with an intent-filter:
*
* <pre>{@literal
* <service android:name="MyJobService"
* android:exported="false">
* <intent-filter>
* <action android:name="MyJobService.action"/>
* <category android:name="android.intent.category.DEFAULT"/>
* </intent-filter>
* </service>
* }</pre>
*
* <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses * <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses
* should be guarded with a call to {@code * should be guarded with a call to {@code
* GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)} * GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
@ -76,44 +57,37 @@ import com.google.android.exoplayer2.util.Util;
public final class JobDispatcherScheduler implements Scheduler { public final class JobDispatcherScheduler implements Scheduler {
private static final String TAG = "JobDispatcherScheduler"; private static final String TAG = "JobDispatcherScheduler";
private static final String SERVICE_ACTION = "SERVICE_ACTION"; private static final String KEY_SERVICE_ACTION = "service_action";
private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE"; private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String REQUIREMENTS = "REQUIREMENTS"; private static final String KEY_REQUIREMENTS = "requirements";
private final String jobTag; private final String jobTag;
private final Job job;
private final FirebaseJobDispatcher jobDispatcher; private final FirebaseJobDispatcher jobDispatcher;
/** /**
* @param context Used to create a {@link FirebaseJobDispatcher} service. * @param context A context.
* @param requirements The requirements to execute the job. * @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous
* @param jobTag Unique tag for the job. Using the same tag as a previous job can cause that job * instance, anything scheduled by the previous instance will be canceled by this instance if
* to be replaced or canceled. * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
* @param serviceAction The action which the service will be started with.
* @param servicePackage The package of the service which contains the logic of the job.
*/ */
public JobDispatcherScheduler( public JobDispatcherScheduler(Context context, String jobTag) {
Context context, this.jobDispatcher =
Requirements requirements, new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
String jobTag,
String serviceAction,
String servicePackage) {
this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
this.jobTag = jobTag; this.jobTag = jobTag;
this.job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
} }
@Override @Override
public boolean schedule() { public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
int result = jobDispatcher.schedule(job); int result = jobDispatcher.schedule(job);
logd("Scheduling JobDispatcher job: " + jobTag + " result: " + result); logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
} }
@Override @Override
public boolean cancel() { public boolean cancel() {
int result = jobDispatcher.cancel(jobTag); int result = jobDispatcher.cancel(jobTag);
logd("Canceling JobDispatcher job: " + jobTag + " result: " + result); logd("Canceling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
} }
@ -151,13 +125,12 @@ public final class JobDispatcherScheduler implements Scheduler {
} }
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true); builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
// Extras, work duration.
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(SERVICE_ACTION, serviceAction); extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(SERVICE_PACKAGE, servicePackage); extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
extras.putInt(REQUIREMENTS, requirements.getRequirementsData()); extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
builder.setExtras(extras); builder.setExtras(extras);
return builder.build(); return builder.build();
} }
@ -167,26 +140,22 @@ public final class JobDispatcherScheduler implements Scheduler {
} }
} }
/** A {@link JobService} to start a service if the requirements are met. */ /** A {@link JobService} that starts the target service if the requirements are met. */
public static final class JobDispatcherSchedulerService extends JobService { public static final class JobDispatcherSchedulerService extends JobService {
@Override @Override
public boolean onStartJob(JobParameters params) { public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started"); logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras(); Bundle extras = params.getExtras();
Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS)); Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) { if (requirements.checkRequirements(this)) {
logd("requirements are met"); logd("Requirements are met");
String serviceAction = extras.getString(SERVICE_ACTION); String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(SERVICE_PACKAGE); String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Intent intent = new Intent(serviceAction).setPackage(servicePackage); Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("starting service action: " + serviceAction + " package: " + servicePackage); logd("Starting service action: " + serviceAction + " package: " + servicePackage);
if (Util.SDK_INT >= 26) { Util.startForegroundService(this, intent);
startForegroundService(intent);
} else { } else {
startService(intent); logd("Requirements are not met");
}
} else {
logd("requirements are not met");
jobFinished(params, /* needsReschedule */ true); jobFinished(params, /* needsReschedule */ true);
} }
return false; return false;

View File

@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private @Nullable PlaybackPreparer playbackPreparer; private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher; private ControlDispatcher controlDispatcher;
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost; private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface; private boolean hasSurface;
private boolean lastNotifiedPreparedState; private boolean lastNotifiedPreparedState;
@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
* @param errorMessageProvider The {@link ErrorMessageProvider}. * @param errorMessageProvider The {@link ErrorMessageProvider}.
*/ */
public void setErrorMessageProvider( public void setErrorMessageProvider(
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) { @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider; this.errorMessageProvider = errorMessageProvider;
} }

View File

@ -334,12 +334,11 @@ public final class MediaSessionConnector {
private Player player; private Player player;
private CustomActionProvider[] customActionProviders; private CustomActionProvider[] customActionProviders;
private Map<String, CustomActionProvider> customActionMap; private Map<String, CustomActionProvider> customActionMap;
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private PlaybackPreparer playbackPreparer; private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator; private QueueNavigator queueNavigator;
private QueueEditor queueEditor; private QueueEditor queueEditor;
private RatingCallback ratingCallback; private RatingCallback ratingCallback;
private ExoPlaybackException playbackException;
/** /**
* Creates an instance. Must be called on the same thread that is used to construct the player * Creates an instance. Must be called on the same thread that is used to construct the player
@ -403,16 +402,18 @@ public final class MediaSessionConnector {
/** /**
* Sets the player to be connected to the media session. * Sets the player to be connected to the media session.
* <p> *
* The order in which any {@link CustomActionProvider}s are passed determines the order of the * <p>The order in which any {@link CustomActionProvider}s are passed determines the order of the
* actions published with the playback state of the session. * actions published with the playback state of the session.
* *
* @param player The player to be connected to the {@code MediaSession}. * @param player The player to be connected to the {@code MediaSession}.
* @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player. * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
* @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle
* custom actions. * custom actions.
*/ */
public void setPlayer(Player player, PlaybackPreparer playbackPreparer, public void setPlayer(
Player player,
@Nullable PlaybackPreparer playbackPreparer,
CustomActionProvider... customActionProviders) { CustomActionProvider... customActionProviders) {
if (this.player != null) { if (this.player != null) {
this.player.removeListener(exoPlayerEventListener); this.player.removeListener(exoPlayerEventListener);
@ -435,13 +436,16 @@ public final class MediaSessionConnector {
} }
/** /**
* Sets the {@link ErrorMessageProvider}. * Sets the optional {@link ErrorMessageProvider}.
* *
* @param errorMessageProvider The error message provider. * @param errorMessageProvider The error message provider.
*/ */
public void setErrorMessageProvider( public void setErrorMessageProvider(
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) { @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider; this.errorMessageProvider = errorMessageProvider;
updateMediaSessionPlaybackState();
}
} }
/** /**
@ -451,10 +455,12 @@ public final class MediaSessionConnector {
* @param queueNavigator The queue navigator. * @param queueNavigator The queue navigator.
*/ */
public void setQueueNavigator(QueueNavigator queueNavigator) { public void setQueueNavigator(QueueNavigator queueNavigator) {
if (this.queueNavigator != queueNavigator) {
unregisterCommandReceiver(this.queueNavigator); unregisterCommandReceiver(this.queueNavigator);
this.queueNavigator = queueNavigator; this.queueNavigator = queueNavigator;
registerCommandReceiver(queueNavigator); registerCommandReceiver(queueNavigator);
} }
}
/** /**
* Sets the {@link QueueEditor} to handle queue edits sent by the media controller. * Sets the {@link QueueEditor} to handle queue edits sent by the media controller.
@ -462,11 +468,13 @@ public final class MediaSessionConnector {
* @param queueEditor The queue editor. * @param queueEditor The queue editor.
*/ */
public void setQueueEditor(QueueEditor queueEditor) { public void setQueueEditor(QueueEditor queueEditor) {
if (this.queueEditor != queueEditor) {
unregisterCommandReceiver(this.queueEditor); unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor; this.queueEditor = queueEditor;
registerCommandReceiver(queueEditor); registerCommandReceiver(queueEditor);
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS mediaSession.setFlags(
: EDITOR_MEDIA_SESSION_FLAGS); queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
}
} }
/** /**
@ -475,10 +483,12 @@ public final class MediaSessionConnector {
* @param ratingCallback The rating callback. * @param ratingCallback The rating callback.
*/ */
public void setRatingCallback(RatingCallback ratingCallback) { public void setRatingCallback(RatingCallback ratingCallback) {
if (this.ratingCallback != ratingCallback) {
unregisterCommandReceiver(this.ratingCallback); unregisterCommandReceiver(this.ratingCallback);
this.ratingCallback = ratingCallback; this.ratingCallback = ratingCallback;
registerCommandReceiver(this.ratingCallback); registerCommandReceiver(this.ratingCallback);
} }
}
private void registerCommandReceiver(CommandReceiver commandReceiver) { private void registerCommandReceiver(CommandReceiver commandReceiver) {
if (commandReceiver != null && commandReceiver.getCommands() != null) { if (commandReceiver != null && commandReceiver.getCommands() != null) {
@ -514,17 +524,17 @@ public final class MediaSessionConnector {
} }
customActionMap = Collections.unmodifiableMap(currentActions); customActionMap = Collections.unmodifiableMap(currentActions);
int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR int playbackState = player.getPlaybackState();
ExoPlaybackException playbackError =
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
int sessionPlaybackState =
playbackError != null
? PlaybackStateCompat.STATE_ERROR
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
if (playbackException != null) { if (playbackError != null && errorMessageProvider != null) {
if (errorMessageProvider != null) { Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackException);
builder.setErrorMessage(message.first, message.second); builder.setErrorMessage(message.first, message.second);
} }
if (player.getPlaybackState() != Player.STATE_IDLE) {
playbackException = null;
}
}
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID; : MediaSessionCompat.QueueItem.UNKNOWN_ID;
Bundle extras = new Bundle(); Bundle extras = new Bundle();
@ -699,12 +709,6 @@ public final class MediaSessionConnector {
updateMediaSessionPlaybackState(); updateMediaSessionPlaybackState();
} }
@Override
public void onPlayerError(ExoPlaybackException error) {
playbackException = error;
updateMediaSessionPlaybackState();
}
@Override @Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (currentWindowIndex != player.getCurrentWindowIndex()) { if (currentWindowIndex != player.getCurrentWindowIndex()) {

View File

@ -73,10 +73,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
/** /**
* Gets the {@link MediaDescriptionCompat} for a given timeline window index. * Gets the {@link MediaDescriptionCompat} for a given timeline window index.
* *
* @param player The current player.
* @param windowIndex The timeline window index for which to provide a description. * @param windowIndex The timeline window index for which to provide a description.
* @return A {@link MediaDescriptionCompat}. * @return A {@link MediaDescriptionCompat}.
*/ */
public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); public abstract MediaDescriptionCompat getMediaDescription(Player player, int windowIndex);
@Override @Override
public long getSupportedQueueNavigatorActions(Player player) { public long getSupportedQueueNavigatorActions(Player player) {
@ -185,7 +186,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
windowCount - queueSize); windowCount - queueSize);
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>(); List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) { for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i)); queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(player, i), i));
} }
mediaSession.setQueue(queue); mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex; activeQueueItemId = currentWindowIndex;

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Herhaal alles"</string> <string name="exo_media_action_repeat_off_description">Herhaal niks</string>
<string name="exo_media_action_repeat_off_description">"Herhaal niks"</string> <string name="exo_media_action_repeat_one_description">Herhaal een</string>
<string name="exo_media_action_repeat_one_description">"Herhaal een"</string> <string name="exo_media_action_repeat_all_description">Herhaal alles</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"ምንም አትድገም"</string> <string name="exo_media_action_repeat_one_description">አንድ ድገም</string>
<string name="exo_media_action_repeat_one_description">"አንዱን ድገም"</string> <string name="exo_media_action_repeat_all_description">ሁሉንም ድገም</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"عدم التكرار"</string> <string name="exo_media_action_repeat_one_description">تكرار مقطع صوتي واحد</string>
<string name="exo_media_action_repeat_one_description">"تكرار مقطع واحد"</string> <string name="exo_media_action_repeat_all_description">تكرار الكل</string>
</resources> </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,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string> <string name="exo_media_action_repeat_off_description">Ne ponavljaj nijednu</string>
<string name="exo_media_action_repeat_off_description">"Ne ponavljaj nijednu"</string> <string name="exo_media_action_repeat_one_description">Ponovi jednu</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jednu"</string> <string name="exo_media_action_repeat_all_description">Ponovi sve</string>
</resources> </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 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"Без повтаряне"</string> <string name="exo_media_action_repeat_one_description">Повтаряне на един елемент</string>
<string name="exo_media_action_repeat_one_description">"Повтаряне на един елемент"</string> <string name="exo_media_action_repeat_all_description">Повтаряне на всички</string>
</resources> </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,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Repeteix-ho tot"</string> <string name="exo_media_action_repeat_off_description">No en repeteixis cap</string>
<string name="exo_media_action_repeat_off_description">"No en repeteixis cap"</string> <string name="exo_media_action_repeat_one_description">Repeteix una</string>
<string name="exo_media_action_repeat_one_description">"Repeteix-ne un"</string> <string name="exo_media_action_repeat_all_description">Repeteix tot</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Opakovat vše"</string> <string name="exo_media_action_repeat_off_description">Neopakovat</string>
<string name="exo_media_action_repeat_off_description">"Neopakovat"</string> <string name="exo_media_action_repeat_one_description">Opakovat jednu</string>
<string name="exo_media_action_repeat_one_description">"Opakovat jednu položku"</string> <string name="exo_media_action_repeat_all_description">Opakovat vše</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Gentag alle"</string> <string name="exo_media_action_repeat_off_description">Gentag ingen</string>
<string name="exo_media_action_repeat_off_description">"Gentag ingen"</string> <string name="exo_media_action_repeat_one_description">Gentag én</string>
<string name="exo_media_action_repeat_one_description">"Gentag en"</string> <string name="exo_media_action_repeat_all_description">Gentag alle</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Alle wiederholen"</string> <string name="exo_media_action_repeat_off_description">Keinen wiederholen</string>
<string name="exo_media_action_repeat_off_description">"Keinen Titel wiederholen"</string> <string name="exo_media_action_repeat_one_description">Einen wiederholen</string>
<string name="exo_media_action_repeat_one_description">"Einen Titel wiederholen"</string> <string name="exo_media_action_repeat_all_description">Alle wiederholen</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"Καμία επανάληψη"</string> <string name="exo_media_action_repeat_one_description">Επανάληψη ενός κομματιού</string>
<string name="exo_media_action_repeat_one_description">"Επανάληψη ενός στοιχείου"</string> <string name="exo_media_action_repeat_all_description">Επανάληψη όλων</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string> <string name="exo_media_action_repeat_off_description">Repeat none</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string> <string name="exo_media_action_repeat_one_description">Repeat one</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string> <string name="exo_media_action_repeat_all_description">Repeat all</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string> <string name="exo_media_action_repeat_off_description">Repeat none</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string> <string name="exo_media_action_repeat_one_description">Repeat one</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string> <string name="exo_media_action_repeat_all_description">Repeat all</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string> <string name="exo_media_action_repeat_off_description">Repeat none</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string> <string name="exo_media_action_repeat_one_description">Repeat one</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string> <string name="exo_media_action_repeat_all_description">Repeat all</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string> <string name="exo_media_action_repeat_off_description">No repetir</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string> <string name="exo_media_action_repeat_one_description">Repetir uno</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string> <string name="exo_media_action_repeat_all_description">Repetir todo</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string> <string name="exo_media_action_repeat_off_description">No repetir</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string> <string name="exo_media_action_repeat_one_description">Repetir uno</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string> <string name="exo_media_action_repeat_all_description">Repetir todo</string>
</resources> </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,21 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"تکرار هیچ‌کدام"</string> <string name="exo_media_action_repeat_one_description">یکبار تکرار</string>
<string name="exo_media_action_repeat_one_description">"یک‌بار تکرار"</string> <string name="exo_media_action_repeat_all_description">تکرار همه</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Toista kaikki"</string> <string name="exo_media_action_repeat_off_description">Ei uudelleentoistoa</string>
<string name="exo_media_action_repeat_off_description">"Toista ei mitään"</string> <string name="exo_media_action_repeat_one_description">Toista yksi uudelleen</string>
<string name="exo_media_action_repeat_one_description">"Toista yksi"</string> <string name="exo_media_action_repeat_all_description">Toista kaikki uudelleen</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string> <string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
<string name="exo_media_action_repeat_off_description">"Aucune répétition"</string> <string name="exo_media_action_repeat_one_description">Lire une chanson en boucle</string>
<string name="exo_media_action_repeat_one_description">"Répéter un élément"</string> <string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string> <string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
<string name="exo_media_action_repeat_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_one_description">"Lire en boucle un élément"</string> <string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
</resources> </resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="exo_media_action_repeat_off_description">Non repetir</string>
<string name="exo_media_action_repeat_one_description">Repetir unha pista</string>
<string name="exo_media_action_repeat_all_description">Repetir todas as pistas</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 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"कुछ भी न दोहराएं"</string> <string name="exo_media_action_repeat_one_description">एक को दोहराएं</string>
<string name="exo_media_action_repeat_one_description">"एक दोहराएं"</string> <string name="exo_media_action_repeat_all_description">सभी को दोहराएं</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string> <string name="exo_media_action_repeat_off_description">Bez ponavljanja</string>
<string name="exo_media_action_repeat_off_description">"Bez ponavljanja"</string> <string name="exo_media_action_repeat_one_description">Ponovi jedno</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jedno"</string> <string name="exo_media_action_repeat_all_description">Ponovi sve</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Összes ismétlése"</string> <string name="exo_media_action_repeat_off_description">Nincs ismétlés</string>
<string name="exo_media_action_repeat_off_description">"Nincs ismétlés"</string> <string name="exo_media_action_repeat_one_description">Egy szám ismétlése</string>
<string name="exo_media_action_repeat_one_description">"Egy ismétlése"</string> <string name="exo_media_action_repeat_all_description">Összes szám ismétlése</string>
</resources> </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 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Ulangi Semua"</string> <string name="exo_media_action_repeat_off_description">Jangan ulangi</string>
<string name="exo_media_action_repeat_off_description">"Jangan Ulangi"</string> <string name="exo_media_action_repeat_one_description">Ulangi 1</string>
<string name="exo_media_action_repeat_one_description">"Ulangi Satu"</string> <string name="exo_media_action_repeat_all_description">Ulangi semua</string>
</resources> </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">"Endurtaka allt"</string>
<string name="exo_media_action_repeat_off_description">"Endurtaka ekkert"</string>
<string name="exo_media_action_repeat_one_description">"Endurtaka eitt"</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="exo_media_action_repeat_off_description">Endurtaka ekkert</string>
<string name="exo_media_action_repeat_one_description">Endurtaka eitt</string>
<string name="exo_media_action_repeat_all_description">Endurtaka allt</string>
</resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Ripeti tutti"</string> <string name="exo_media_action_repeat_off_description">Non ripetere nulla</string>
<string name="exo_media_action_repeat_off_description">"Non ripetere nessuno"</string> <string name="exo_media_action_repeat_one_description">Ripeti uno</string>
<string name="exo_media_action_repeat_one_description">"Ripeti uno"</string> <string name="exo_media_action_repeat_all_description">Ripeti tutto</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"אל תחזור על כלום"</string> <string name="exo_media_action_repeat_one_description">חזור על פריט אחד</string>
<string name="exo_media_action_repeat_one_description">"חזור על פריט אחד"</string> <string name="exo_media_action_repeat_all_description">חזור על הכול</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"繰り返しなし"</string> <string name="exo_media_action_repeat_one_description">1 曲をリピート</string>
<string name="exo_media_action_repeat_one_description">"1曲を繰り返し"</string> <string name="exo_media_action_repeat_all_description">全曲をリピート</string>
</resources> </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">"Барлығын қайталау"</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">"ធ្វើ​ម្ដង​ទៀត​ទាំងអស់"</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">"ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ"</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 +1,6 @@
<?xml version="1.0"?> <?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> <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_off_description">"반복 안함"</string> <string name="exo_media_action_repeat_one_description">현재 미디어 반복</string>
<string name="exo_media_action_repeat_one_description">"한 항목 반복"</string> <string name="exo_media_action_repeat_all_description">모두 반복</string>
</resources> </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">"ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ"</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 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Kartoti viską"</string> <string name="exo_media_action_repeat_off_description">Nekartoti nieko</string>
<string name="exo_media_action_repeat_off_description">"Nekartoti nieko"</string> <string name="exo_media_action_repeat_one_description">Kartoti vieną</string>
<string name="exo_media_action_repeat_one_description">"Kartoti vieną"</string> <string name="exo_media_action_repeat_all_description">Kartoti viską</string>
</resources> </resources>

View File

@ -1,21 +1,6 @@
<?xml version="1.0"?> <?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> <resources>
<string name="exo_media_action_repeat_all_description">"Atkārtot visu"</string> <string name="exo_media_action_repeat_off_description">Neatkārtot nevienu</string>
<string name="exo_media_action_repeat_off_description">"Neatkārtot nevienu"</string> <string name="exo_media_action_repeat_one_description">Atkārtot vienu</string>
<string name="exo_media_action_repeat_one_description">"Atkārtot vienu"</string> <string name="exo_media_action_repeat_all_description">Atkārtot visu</string>
</resources> </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">"എല്ലാം ആവർത്തിക്കുക"</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">"Бүгдийг давтах"</string>
<string name="exo_media_action_repeat_off_description">"Алийг нь ч давтахгүй"</string>
<string name="exo_media_action_repeat_one_description">"Нэгийг давтах"</string>
</resources>

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