Commit 000f3f23 by ojw28 Committed by GitHub

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

r2.8.0
parents 4b531dcc 5912707d
Showing with 1598 additions and 841 deletions
...@@ -43,6 +43,9 @@ cmake-build-debug ...@@ -43,6 +43,9 @@ cmake-build-debug
dist dist
tmp tmp
# External native builds
.externalNativeBuild
# VP9 extension # VP9 extension
extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
...@@ -62,3 +65,4 @@ extensions/cronet/jniLibs/* ...@@ -62,3 +65,4 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
...@@ -38,4 +38,6 @@ devices and Android versions. ...@@ -38,4 +38,6 @@ devices and Android versions.
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file. log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format "Issue #1234". bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234".
...@@ -38,8 +38,8 @@ repositories { ...@@ -38,8 +38,8 @@ repositories {
} }
``` ```
Next add a gradle compile dependency to the `build.gradle` file of your app Next add a dependency in the `build.gradle` file of your app module. The
module. The following will add a dependency to the full library: following will add a dependency to the full library:
```gradle ```gradle
implementation 'com.google.android.exoplayer:exoplayer:2.X.X' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
......
# Release notes # # Release notes #
### 2.8.0 ###
* Downloading:
* Add `DownloadService`, `DownloadManager` and related classes
([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on
using these components to download progressive formats can be found
[here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95).
To see how to download DASH, HLS and SmoothStreaming media, take a look at
the app.
* Updated main demo app to support downloading DASH, HLS, SmoothStreaming and
progressive media.
* MediaSources:
* Allow reusing media sources after they have been released and
also in parallel to allow adding them multiple times to a concatenation.
([#3498](https://github.com/google/ExoPlayer/issues/3498)).
* Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and
deprecated `DynamicConcatenatingMediaSource`.
* Allow clipping of child media sources where the period and window have a
non-zero offset with `ClippingMediaSource`.
* Allow adding and removing `MediaSourceEventListener`s to MediaSources after
they have been created. Listening to events is now supported for all
media sources including composite sources.
* Added callbacks to `MediaSourceEventListener` to get notified when media
periods are created, released and being read from.
* Support live stream clipping with `ClippingMediaSource`.
* Allow setting tags for all media sources in their factories. The tag of the
current window can be retrieved with `ExoPlayer.getCurrentTag`.
* UI components:
* Add support for displaying error messages and a buffering spinner in
`PlayerView`.
* Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
([#3736](https://github.com/google/ExoPlayer/issues/3736)).
* Add `PlayerNotificationManager` for displaying notifications reflecting the
player state.
* Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`.
* Add `TrackNameProvider` for converting track `Format`s to textual
descriptions, and `DefaultTrackNameProvider` as a default implementation.
* Track selection:
* Reworked `MappingTrackSelector` and `DefaultTrackSelector`.
* `DefaultTrackSelector.Parameters` now implements `Parcelable`.
* Added UI components for track selection (see above).
* Audio:
* Support extracting data from AMR container formats, including both narrow
and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)).
* FLAC:
* Sniff FLAC files correctly if they have ID3 headers
([#4055](https://github.com/google/ExoPlayer/issues/4055)).
* Supports FLAC files with high sample rate (176400 and 192000)
([#3769](https://github.com/google/ExoPlayer/issues/3769)).
* Factor out `AudioTrack` position tracking from `DefaultAudioSink`.
* Fix an issue where the playback position would pause just after playback
begins, and poll the audio timestamp less frequently once it starts
advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).
* Add an option to skip silent audio in `PlaybackParameters`
((#2635)[https://github.com/google/ExoPlayer/issues/2635]).
* Fix an issue where playback of TrueHD streams would get stuck after seeking
due to not finding a syncframe
((#3845)[https://github.com/google/ExoPlayer/issues/3845]).
* Fix an issue with eac3-joc playback where a codec would fail to configure
((#4165)[https://github.com/google/ExoPlayer/issues/4165]).
* Handle non-empty end-of-stream buffers, to fix gapless playback of streams
with encoder padding when the decoder returns a non-empty final buffer.
* Allow trimming more than one sample when applying an elst audio edit via
gapless playback info.
* Allow overriding skipping/scaling with custom `AudioProcessor`s
((#3142)[https://github.com/google/ExoPlayer/issues/3142]).
* Caching:
* Add release method to the `Cache` interface, and prevent multiple instances
of `SimpleCache` using the same folder at the same time.
* Cache redirect URLs
([#2360](https://github.com/google/ExoPlayer/issues/2360)).
* DRM:
* Allow multiple listeners for `DefaultDrmSessionManager`.
* Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.
* Change minimum API requirement for CBC and pattern encryption from 24 to 25
([#4022][https://github.com/google/ExoPlayer/issues/4022]).
* Fix handling of 307/308 redirects when making license requests
([#4108](https://github.com/google/ExoPlayer/issues/4108)).
* HLS:
* Fix playlist loading error propagation when the current selection does
not include all of the playlist's variants.
* Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods
([#4145](https://github.com/google/ExoPlayer/issues/4145)).
* Preeptively declare an ID3 track in chunkless preparation
([#4016](https://github.com/google/ExoPlayer/issues/4016)).
* Add support for multiple #EXT-X-MAP tags in a media playlist
([#4164](https://github.com/google/ExoPlayer/issues/4182)).
* Fix seeking in live streams
([#4187](https://github.com/google/ExoPlayer/issues/4187)).
* IMA:
* Allow setting the ad media load timeout
([#3691](https://github.com/google/ExoPlayer/issues/3691)).
* Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,
and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the
`AdsMediaSource.EventListener`.
* Add `AnalyticsListener` interface which can be registered in
`SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event.
* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within
a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
* Updated default max buffer length in `DefaultLoadControl`.
* Fix ClearKey decryption error if the key contains a forward slash
([#4075](https://github.com/google/ExoPlayer/issues/4075)).
* Fix crash when switching surface on Huawei P9 Lite
([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E
([#4104](https://github.com/google/ExoPlayer/issues/4104)).
* Support ZLIB compressed PGS subtitles.
* Added `getPlaybackError` to `Player` interface.
* Moved initial bitrate estimate from `AdaptiveTrackSelection` to
`DefaultBandwidthMeter`.
* Removed default renderer time offset of 60000000 from internal player. The
actual renderer timestamp offset can be obtained by listening to
`BaseRenderer.onStreamChanged`.
* Added dependencies on checkerframework annotations for static code analysis.
### 2.7.3 ### ### 2.7.3 ###
* Fix ProGuard configuration for Cast, IMA and OkHttp extensions. * Fix ProGuard configuration for Cast, IMA and OkHttp extensions.
...@@ -93,7 +207,7 @@ ...@@ -93,7 +207,7 @@
([#3630](https://github.com/google/ExoPlayer/issues/3630)). ([#3630](https://github.com/google/ExoPlayer/issues/3630)).
* DASH: * DASH:
* Support in-band Emsg events targeting the player with scheme id * Support in-band Emsg events targeting the player with scheme id
"urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3". `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3".
* Support EventStream elements in DASH manifests. * Support EventStream elements in DASH manifests.
* HLS: * HLS:
* Add opt-in support for chunkless preparation in HLS. This allows an * Add opt-in support for chunkless preparation in HLS. This allows an
...@@ -163,6 +277,7 @@ ...@@ -163,6 +277,7 @@
([#3792](https://github.com/google/ExoPlayer/issues/3792). ([#3792](https://github.com/google/ExoPlayer/issues/3792).
* Support 14-bit mode and little endianness in DTS PES packets * Support 14-bit mode and little endianness in DTS PES packets
([#3340](https://github.com/google/ExoPlayer/issues/3340)). ([#3340](https://github.com/google/ExoPlayer/issues/3340)).
* Demo app: Add ability to download not DRM protected content.
### 2.6.1 ### ### 2.6.1 ###
......
<?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 @@ ...@@ -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">"Ponovite sve"</string> <issue id="InvalidPackage">
<string name="exo_media_action_repeat_off_description">"Ne ponavljaju"</string> <ignore path="**/checker-qual-*.jar"/>
<string name="exo_media_action_repeat_one_description">"Ponovite jedan"</string> </issue>
</resources> </lint>
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.7.3' releaseVersion = '2.8.0'
releaseVersionCode = 2703 releaseVersionCode = 2800
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
...@@ -25,12 +25,14 @@ project.ext { ...@@ -25,12 +25,14 @@ project.ext {
buildToolsVersion = '27.0.3' buildToolsVersion = '27.0.3'
testSupportLibraryVersion = '0.5' testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.0.0' supportLibraryVersion = '27.0.0'
playServicesLibraryVersion = '11.4.2' playServicesLibraryVersion = '12.0.0'
dexmakerVersion = '1.2' dexmakerVersion = '1.2'
mockitoVersion = '1.9.5' mockitoVersion = '1.9.5'
junitVersion = '4.12' junitVersion = '4.12'
truthVersion = '0.39' truthVersion = '0.39'
robolectricVersion = '3.7.1' robolectricVersion = '3.7.1'
autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -36,6 +36,7 @@ include modulePrefix + 'extension-opus' ...@@ -36,6 +36,7 @@ include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
...@@ -56,6 +57,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi ...@@ -56,6 +57,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
if (gradle.ext.has('exoplayerIncludeCronetExtension') if (gradle.ext.has('exoplayerIncludeCronetExtension')
&& gradle.ext.exoplayerIncludeCronetExtension) { && gradle.ext.exoplayerIncludeCronetExtension) {
......
...@@ -32,7 +32,7 @@ import com.google.android.exoplayer2.Timeline; ...@@ -32,7 +32,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
...@@ -80,8 +80,8 @@ import java.util.ArrayList; ...@@ -80,8 +80,8 @@ import java.util.ArrayList;
private final CastPlayer castPlayer; private final CastPlayer castPlayer;
private final ArrayList<DemoUtil.Sample> mediaQueue; private final ArrayList<DemoUtil.Sample> mediaQueue;
private final QueuePositionListener queuePositionListener; private final QueuePositionListener queuePositionListener;
private final ConcatenatingMediaSource concatenatingMediaSource;
private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
private boolean castMediaQueueCreationPending; private boolean castMediaQueueCreationPending;
private int currentItemIndex; private int currentItemIndex;
private Player currentPlayer; private Player currentPlayer;
...@@ -117,9 +117,10 @@ import java.util.ArrayList; ...@@ -117,9 +117,10 @@ import java.util.ArrayList;
this.castControlView = castControlView; this.castControlView = castControlView;
mediaQueue = new ArrayList<>(); mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET; currentItemIndex = C.INDEX_UNSET;
concatenatingMediaSource = new ConcatenatingMediaSource();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
exoPlayer.addListener(this); exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer); localPlayerView.setPlayer(exoPlayer);
...@@ -155,9 +156,8 @@ import java.util.ArrayList; ...@@ -155,9 +156,8 @@ import java.util.ArrayList;
*/ */
public void addItem(Sample sample) { public void addItem(Sample sample) {
mediaQueue.add(sample); mediaQueue.add(sample);
if (currentPlayer == exoPlayer) { concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); if (currentPlayer == castPlayer) {
} else {
castPlayer.addItems(buildMediaQueueItem(sample)); castPlayer.addItems(buildMediaQueueItem(sample));
} }
} }
...@@ -186,9 +186,8 @@ import java.util.ArrayList; ...@@ -186,9 +186,8 @@ import java.util.ArrayList;
* @return Whether the removal was successful. * @return Whether the removal was successful.
*/ */
public boolean removeItem(int itemIndex) { public boolean removeItem(int itemIndex) {
if (currentPlayer == exoPlayer) { concatenatingMediaSource.removeMediaSource(itemIndex);
dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); if (currentPlayer == castPlayer) {
} else {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline(); Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) { if (castTimeline.getPeriodCount() <= itemIndex) {
...@@ -215,9 +214,8 @@ import java.util.ArrayList; ...@@ -215,9 +214,8 @@ import java.util.ArrayList;
*/ */
public boolean moveItem(int fromIndex, int toIndex) { public boolean moveItem(int fromIndex, int toIndex) {
// Player update. // Player update.
if (currentPlayer == exoPlayer) { concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
} else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline(); Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount(); int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) { if (periodCount <= fromIndex || periodCount <= toIndex) {
...@@ -263,6 +261,7 @@ import java.util.ArrayList; ...@@ -263,6 +261,7 @@ import java.util.ArrayList;
public void release() { public void release() {
currentItemIndex = C.INDEX_UNSET; currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear(); mediaQueue.clear();
concatenatingMediaSource.clear();
castPlayer.setSessionAvailabilityListener(null); castPlayer.setSessionAvailabilityListener(null);
castPlayer.release(); castPlayer.release();
localPlayerView.setPlayer(null); localPlayerView.setPlayer(null);
...@@ -354,11 +353,7 @@ import java.util.ArrayList; ...@@ -354,11 +353,7 @@ import java.util.ArrayList;
// Media queue management. // Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer; castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) { if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); exoPlayer.prepare(concatenatingMediaSource);
for (int i = 0; i < mediaQueue.size(); i++) {
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
}
exoPlayer.prepare(dynamicConcatenatingMediaSource);
} }
// Playback transition. // Playback transition.
......
...@@ -17,8 +17,6 @@ package com.google.android.exoplayer2.imademo; ...@@ -17,8 +17,6 @@ package com.google.android.exoplayer2.imademo;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
...@@ -27,7 +25,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer; ...@@ -27,7 +25,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
...@@ -83,8 +80,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -83,8 +80,7 @@ import com.google.android.exoplayer2.util.Util;
// This is the MediaSource representing the content media (i.e. not the ad). // This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url); String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource = MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null);
// Compose the content media source into a new AdsMediaSource with both ads and content. // Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds = MediaSource mediaSourceWithAds =
...@@ -121,9 +117,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -121,9 +117,8 @@ import com.google.android.exoplayer2.util.Util;
// AdsMediaSource.MediaSourceFactory implementation. // AdsMediaSource.MediaSourceFactory implementation.
@Override @Override
public MediaSource createMediaSource( public MediaSource createMediaSource(Uri uri) {
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { return buildMediaSource(uri);
return buildMediaSource(uri, handler, listener);
} }
@Override @Override
...@@ -134,25 +129,22 @@ import com.google.android.exoplayer2.util.Util; ...@@ -134,25 +129,22 @@ import com.google.android.exoplayer2.util.Util;
// Internal methods. // Internal methods.
private MediaSource buildMediaSource( private MediaSource buildMediaSource(Uri uri) {
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
@ContentType int type = Util.inferContentType(uri); @ContentType int type = Util.inferContentType(uri);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashMediaSource.Factory( return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
manifestDataSourceFactory) manifestDataSourceFactory)
.createMediaSource(uri, handler, listener); .createMediaSource(uri);
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource.Factory( return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
.createMediaSource(uri, handler, listener); .createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory) return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
.createMediaSource(uri, handler, listener);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(mediaDataSourceFactory) return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
.createMediaSource(uri, handler, listener);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
......
...@@ -19,6 +19,8 @@ ...@@ -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 @@ ...@@ -73,6 +75,18 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
<service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application> </application>
</manifest> </manifest>
...@@ -578,5 +578,16 @@ ...@@ -578,5 +578,16 @@
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2" "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
} }
] ]
},
{
"name": "ABR",
"samples": [
{
"name": "Random ABR - Google Glass (MP4,H264)",
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"extension": "mpd",
"abr_algorithm": "random"
}
]
} }
] ]
...@@ -16,20 +16,51 @@ ...@@ -16,20 +16,51 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.ProgressiveDownloadAction;
import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction;
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction;
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File;
/** /**
* Placeholder application to facilitate overriding Application methods for debugging and testing. * Placeholder application to facilitate overriding Application methods for debugging and testing.
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
private static final Deserializer[] DOWNLOAD_DESERIALIZERS =
new Deserializer[] {
DashDownloadAction.DESERIALIZER,
HlsDownloadAction.DESERIALIZER,
SsDownloadAction.DESERIALIZER,
ProgressiveDownloadAction.DESERIALIZER
};
protected String userAgent; protected String userAgent;
private File downloadDirectory;
private Cache downloadCache;
private DownloadManager downloadManager;
private DownloadTracker downloadTracker;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
...@@ -38,7 +69,9 @@ public class DemoApplication extends Application { ...@@ -38,7 +69,9 @@ public class DemoApplication extends Application {
/** Returns a {@link DataSource.Factory}. */ /** Returns a {@link DataSource.Factory}. */
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) { public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); DefaultDataSourceFactory upstreamFactory =
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
} }
/** Returns a {@link HttpDataSource.Factory}. */ /** Returns a {@link HttpDataSource.Factory}. */
...@@ -47,8 +80,69 @@ public class DemoApplication extends Application { ...@@ -47,8 +80,69 @@ public class DemoApplication extends Application {
return new DefaultHttpDataSourceFactory(userAgent, listener); return new DefaultHttpDataSourceFactory(userAgent, listener);
} }
/** Returns whether extension renderers should be used. */
public boolean useExtensionRenderers() { public boolean useExtensionRenderers() {
return BuildConfig.FLAVOR.equals("withExtensions"); return "withExtensions".equals(BuildConfig.FLAVOR);
}
public DownloadManager getDownloadManager() {
initDownloadManager();
return downloadManager;
} }
public DownloadTracker getDownloadTracker() {
initDownloadManager();
return downloadTracker;
}
private synchronized void initDownloadManager() {
if (downloadManager == null) {
DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(
getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null));
downloadManager =
new DownloadManager(
downloaderConstructorHelper,
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
DOWNLOAD_DESERIALIZERS);
downloadTracker =
new DownloadTracker(
/* context= */ this,
buildDataSourceFactory(/* listener= */ null),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE),
DOWNLOAD_DESERIALIZERS);
downloadManager.addListener(downloadTracker);
}
}
private synchronized Cache getDownloadCache() {
if (downloadCache == null) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
}
return downloadCache;
}
private File getDownloadDirectory() {
if (downloadDirectory == null) {
downloadDirectory = getExternalFilesDir(null);
if (downloadDirectory == null) {
downloadDirectory = getFilesDir();
}
}
return downloadDirectory;
}
private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory(
cache,
upstreamFactory,
new FileDataSourceFactory(),
/* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/* eventListener= */ null);
}
} }
/*
* 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);
}
}
/*
* 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() {}
}
# Proguard rules specific to the main demo app.
# Constructor accessed via reflection in PlayerActivity
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
<init>(android.content.Context, android.net.Uri);
}
<?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>
<?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 @@ ...@@ -14,8 +13,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources> <ListView xmlns:android="http://schemas.android.com/apk/res/android"
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string> android:id="@+id/representation_list"
<string name="exo_media_action_repeat_off_description">"Non repetir"</string> android:layout_width="match_parent"
<string name="exo_media_action_repeat_one_description">"Repetir un"</string> android:layout_height="match_parent"/>
</resources>
...@@ -17,19 +17,11 @@ ...@@ -17,19 +17,11 @@
<string name="application_name">ExoPlayer</string> <string name="application_name">ExoPlayer</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="text">Text</string>
<string name="selection_disabled">Disabled</string>
<string name="selection_default">Default</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string> <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="enable_random_adaptation">Enable random adaptation</string> <string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string> <string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
...@@ -55,4 +47,14 @@ ...@@ -55,4 +47,14 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string>
</resources> </resources>
...@@ -30,9 +30,9 @@ dependencies { ...@@ -30,9 +30,9 @@ dependencies {
// com.android.support:support-v4, com.android.support:appcompat-v7 and // com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are // com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example: // used, for example:
// com.google.android.gms:play-services-cast-framework:11.4.2 // com.google.android.gms:play-services-cast-framework:12.0.0
// |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.google.android.gms:play-services-basement:12.0.0
// |-- com.android.support:support-v4:25.2.0 // |-- com.android.support:support-v4:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:appcompat-v7:' + supportLibraryVersion api 'com.android.support:appcompat-v7:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
......
...@@ -19,6 +19,7 @@ import android.support.annotation.NonNull; ...@@ -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;
...@@ -308,6 +309,11 @@ public final class CastPlayer implements Player { ...@@ -308,6 +309,11 @@ public final class CastPlayer implements Player {
} }
@Override @Override
public ExoPlaybackException getPlaybackError() {
return null;
}
@Override
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
return; return;
...@@ -481,6 +487,14 @@ public final class CastPlayer implements Player { ...@@ -481,6 +487,14 @@ public final class CastPlayer implements Player {
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false); : currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
} }
@Override
public @Nullable Object getCurrentTag() {
int windowIndex = getCurrentWindowIndex();
return windowIndex > currentTimeline.getWindowCount()
? null
: currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
}
// TODO: Fill the cast timeline information with ProgressListener's duration updates. // TODO: Fill the cast timeline information with ProgressListener's duration updates.
// See [Internal: b/65152553]. // See [Internal: b/65152553].
@Override @Override
......
...@@ -73,12 +73,22 @@ import java.util.Map; ...@@ -73,12 +73,22 @@ import java.util.Map;
} }
@Override @Override
public Window getWindow(int windowIndex, Window window, boolean setIds, public Window getWindow(
long defaultPositionProjectionUs) { int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex]; long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic, Object tag = setTag ? ids[windowIndex] : null;
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0); return window.set(
tag,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
isDynamic,
defaultPositionsUs[windowIndex],
durationUs,
/* firstPeriodIndex= */ windowIndex,
/* lastPeriodIndex= */ windowIndex,
/* positionInFirstPeriodUs= */ 0);
} }
@Override @Override
......
...@@ -89,7 +89,7 @@ import com.google.android.gms.cast.MediaTrack; ...@@ -89,7 +89,7 @@ import com.google.android.gms.cast.MediaTrack;
case CastStatusCodes.UNKNOWN_ERROR: case CastStatusCodes.UNKNOWN_ERROR:
return "An unknown, unexpected error has occurred."; return "An unknown, unexpected error has occurred.";
default: default:
return "Unknown: " + statusCode; return CastStatusCodes.getStatusCodeString(statusCode);
} }
} }
......
...@@ -14,9 +14,4 @@ ...@@ -14,9 +14,4 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest package="com.google.android.exoplayer2.ext.cast.test"/>
package="com.google.android.exoplayer2.ext.cast.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
</manifest>
...@@ -280,6 +280,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -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 ...@@ -352,17 +353,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
if (!operation.block(readTimeoutMs)) { if (!operation.block(readTimeoutMs)) {
throw new SocketTimeoutException(); throw new SocketTimeoutException();
} }
} catch (InterruptedException | SocketTimeoutException e) { } catch (InterruptedException e) {
// If we're timing out or getting interrupted, the operation is still ongoing. // The operation is ongoing so replace readBuffer to avoid it being written to by this
// So we'll need to replace readBuffer to avoid the possibility of it being written to by // operation during a subsequent request.
// this operation during a subsequent request.
readBuffer = null; readBuffer = null;
Thread.currentThread().interrupt();
throw new HttpDataSourceException( throw new HttpDataSourceException(
e instanceof InterruptedException new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
? new InterruptedIOException((InterruptedException) e) } catch (SocketTimeoutException e) {
: (SocketTimeoutException) e, // The operation is ongoing so replace readBuffer to avoid it being written to by this
currentDataSpec, // operation during a subsequent request.
HttpDataSourceException.TYPE_READ); readBuffer = null;
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
} }
if (exception != null) { if (exception != null) {
......
...@@ -21,6 +21,7 @@ import android.util.Log; ...@@ -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 { ...@@ -86,7 +87,7 @@ public final class CronetEngineWrapper {
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) { public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null; CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE; @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context); List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
// Remove disabled and fallback Cronet providers from list // Remove disabled and fallback Cronet providers from list
for (int i = cronetProviders.size() - 1; i >= 0; i--) { for (int i = cronetProviders.size() - 1; i >= 0; i--) {
if (!cronetProviders.get(i).isEnabled() if (!cronetProviders.get(i).isEnabled()
......
...@@ -14,9 +14,4 @@ ...@@ -14,9 +14,4 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest package="com.google.android.exoplayer2.ext.cronet"/>
package="com.google.android.exoplayer2.ext.cronet">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
</manifest>
...@@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
*/ */
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioSink audioSink, boolean enableFloatOutput) { AudioSink audioSink, boolean enableFloatOutput) {
super(eventHandler, eventListener, null, false, audioSink); super(
eventHandler,
eventListener,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
audioSink);
this.enableFloatOutput = enableFloatOutput; this.enableFloatOutput = enableFloatOutput;
} }
......
...@@ -31,8 +31,10 @@ android { ...@@ -31,8 +31,10 @@ android {
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
ext { ext {
......
...@@ -18,8 +18,6 @@ ...@@ -18,8 +18,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test"> package="com.google.android.exoplayer2.ext.flac.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
<application android:debuggable="true" <application android:debuggable="true"
android:allowBackup="false" android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
......
...@@ -13,13 +13,13 @@ track 0: ...@@ -13,13 +13,13 @@ track 0:
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0
rotationDegrees = -1 rotationDegrees = 0
pixelWidthHeightRatio = -1.0 pixelWidthHeightRatio = 1.0
channelCount = 2 channelCount = 2
sampleRate = 48000 sampleRate = 48000
pcmEncoding = 2 pcmEncoding = 2
encoderDelay = -1 encoderDelay = 0
encoderPadding = -1 encoderPadding = 0
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = null language = null
......
...@@ -13,13 +13,13 @@ track 0: ...@@ -13,13 +13,13 @@ track 0:
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0
rotationDegrees = -1 rotationDegrees = 0
pixelWidthHeightRatio = -1.0 pixelWidthHeightRatio = 1.0
channelCount = 2 channelCount = 2
sampleRate = 48000 sampleRate = 48000
pcmEncoding = 2 pcmEncoding = 2
encoderDelay = -1 encoderDelay = 0
encoderPadding = -1 encoderPadding = 0
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = null language = null
......
...@@ -13,13 +13,13 @@ track 0: ...@@ -13,13 +13,13 @@ track 0:
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0
rotationDegrees = -1 rotationDegrees = 0
pixelWidthHeightRatio = -1.0 pixelWidthHeightRatio = 1.0
channelCount = 2 channelCount = 2
sampleRate = 48000 sampleRate = 48000
pcmEncoding = 2 pcmEncoding = 2
encoderDelay = -1 encoderDelay = 0
encoderPadding = -1 encoderPadding = 0
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = null language = null
......
...@@ -13,13 +13,13 @@ track 0: ...@@ -13,13 +13,13 @@ track 0:
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0
rotationDegrees = -1 rotationDegrees = 0
pixelWidthHeightRatio = -1.0 pixelWidthHeightRatio = 1.0
channelCount = 2 channelCount = 2
sampleRate = 48000 sampleRate = 48000
pcmEncoding = 2 pcmEncoding = 2
encoderDelay = -1 encoderDelay = 0
encoderPadding = -1 encoderPadding = 0
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = null language = null
......
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 526272
sample count = 33
sample 0:
time = 0
flags = 1
data = length 16384, hash 61D2C5C2
sample 1:
time = 85333
flags = 1
data = length 16384, hash E6D7F214
sample 2:
time = 170666
flags = 1
data = length 16384, hash 59BF0D5D
sample 3:
time = 256000
flags = 1
data = length 16384, hash 3625F468
sample 4:
time = 341333
flags = 1
data = length 16384, hash F66A323
sample 5:
time = 426666
flags = 1
data = length 16384, hash CDBAE629
sample 6:
time = 512000
flags = 1
data = length 16384, hash 536F3A91
sample 7:
time = 597333
flags = 1
data = length 16384, hash D4F35C9C
sample 8:
time = 682666
flags = 1
data = length 16384, hash EE04CEBF
sample 9:
time = 768000
flags = 1
data = length 16384, hash 647E2A67
sample 10:
time = 853333
flags = 1
data = length 16384, hash 31583F2C
sample 11:
time = 938666
flags = 1
data = length 16384, hash E433A93D
sample 12:
time = 1024000
flags = 1
data = length 16384, hash 5E1C7051
sample 13:
time = 1109333
flags = 1
data = length 16384, hash 43E6E358
sample 14:
time = 1194666
flags = 1
data = length 16384, hash 5DC1B256
sample 15:
time = 1280000
flags = 1
data = length 16384, hash 3D9D95CF
sample 16:
time = 1365333
flags = 1
data = length 16384, hash 2A5BD2C0
sample 17:
time = 1450666
flags = 1
data = length 16384, hash 93E25061
sample 18:
time = 1536000
flags = 1
data = length 16384, hash B81793D8
sample 19:
time = 1621333
flags = 1
data = length 16384, hash 1A3BD49F
sample 20:
time = 1706666
flags = 1
data = length 16384, hash FB672FF1
sample 21:
time = 1792000
flags = 1
data = length 16384, hash 48AB8B45
sample 22:
time = 1877333
flags = 1
data = length 16384, hash 13C9640A
sample 23:
time = 1962666
flags = 1
data = length 16384, hash 499E4A0B
sample 24:
time = 2048000
flags = 1
data = length 16384, hash F9A783E6
sample 25:
time = 2133333
flags = 1
data = length 16384, hash D2B77598
sample 26:
time = 2218666
flags = 1
data = length 16384, hash CE5B826C
sample 27:
time = 2304000
flags = 1
data = length 16384, hash E99EE956
sample 28:
time = 2389333
flags = 1
data = length 16384, hash F2DB1486
sample 29:
time = 2474666
flags = 1
data = length 16384, hash 1636EAB
sample 30:
time = 2560000
flags = 1
data = length 16384, hash 23457C08
sample 31:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 32:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 362432
sample count = 23
sample 0:
time = 853333
flags = 1
data = length 16384, hash 31583F2C
sample 1:
time = 938666
flags = 1
data = length 16384, hash E433A93D
sample 2:
time = 1024000
flags = 1
data = length 16384, hash 5E1C7051
sample 3:
time = 1109333
flags = 1
data = length 16384, hash 43E6E358
sample 4:
time = 1194666
flags = 1
data = length 16384, hash 5DC1B256
sample 5:
time = 1280000
flags = 1
data = length 16384, hash 3D9D95CF
sample 6:
time = 1365333
flags = 1
data = length 16384, hash 2A5BD2C0
sample 7:
time = 1450666
flags = 1
data = length 16384, hash 93E25061
sample 8:
time = 1536000
flags = 1
data = length 16384, hash B81793D8
sample 9:
time = 1621333
flags = 1
data = length 16384, hash 1A3BD49F
sample 10:
time = 1706666
flags = 1
data = length 16384, hash FB672FF1
sample 11:
time = 1792000
flags = 1
data = length 16384, hash 48AB8B45
sample 12:
time = 1877333
flags = 1
data = length 16384, hash 13C9640A
sample 13:
time = 1962666
flags = 1
data = length 16384, hash 499E4A0B
sample 14:
time = 2048000
flags = 1
data = length 16384, hash F9A783E6
sample 15:
time = 2133333
flags = 1
data = length 16384, hash D2B77598
sample 16:
time = 2218666
flags = 1
data = length 16384, hash CE5B826C
sample 17:
time = 2304000
flags = 1
data = length 16384, hash E99EE956
sample 18:
time = 2389333
flags = 1
data = length 16384, hash F2DB1486
sample 19:
time = 2474666
flags = 1
data = length 16384, hash 1636EAB
sample 20:
time = 2560000
flags = 1
data = length 16384, hash 23457C08
sample 21:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 22:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 182208
sample count = 12
sample 0:
time = 1792000
flags = 1
data = length 16384, hash 48AB8B45
sample 1:
time = 1877333
flags = 1
data = length 16384, hash 13C9640A
sample 2:
time = 1962666
flags = 1
data = length 16384, hash 499E4A0B
sample 3:
time = 2048000
flags = 1
data = length 16384, hash F9A783E6
sample 4:
time = 2133333
flags = 1
data = length 16384, hash D2B77598
sample 5:
time = 2218666
flags = 1
data = length 16384, hash CE5B826C
sample 6:
time = 2304000
flags = 1
data = length 16384, hash E99EE956
sample 7:
time = 2389333
flags = 1
data = length 16384, hash F2DB1486
sample 8:
time = 2474666
flags = 1
data = length 16384, hash 1636EAB
sample 9:
time = 2560000
flags = 1
data = length 16384, hash 23457C08
sample 10:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 11:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = [[timeUs=0, position=55284]]
numberOfTracks = 1
track 0:
format:
bitrate = 768000
id = null
containerMimeType = null
sampleMimeType = audio/raw
maxInputSize = 16384
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 18368
sample count = 2
sample 0:
time = 2645333
flags = 1
data = length 16384, hash 30EB8381
sample 1:
time = 2730666
flags = 1
data = length 1984, hash 59CFDE1B
tracksEnded = true
...@@ -33,7 +33,7 @@ public class FlacExtractorTest extends InstrumentationTestCase { ...@@ -33,7 +33,7 @@ public class FlacExtractorTest extends InstrumentationTestCase {
} }
} }
public void testSample() throws Exception { public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
new ExtractorFactory() { new ExtractorFactory() {
@Override @Override
...@@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase { ...@@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase {
"bear.flac", "bear.flac",
getInstrumentation().getContext()); getInstrumentation().getContext());
} }
public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
}
},
"bear_with_id3.flac",
getInstrumentation().getContext());
}
} }
...@@ -17,20 +17,27 @@ package com.google.android.exoplayer2.ext.flac; ...@@ -17,20 +17,27 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.android.exoplayer2.util.Util.getPcmEncoding; import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.Id3Peeker;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
...@@ -51,22 +58,56 @@ public final class FlacExtractor implements Extractor { ...@@ -51,22 +58,56 @@ public final class FlacExtractor implements Extractor {
}; };
/** Flags controlling the behavior of the extractor. */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_DISABLE_ID3_METADATA}
)
public @interface Flags {}
/**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required.
*/
public static final int FLAG_DISABLE_ID3_METADATA = 1;
/** /**
* FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
* mandatory STREAMINFO. * mandatory STREAMINFO.
*/ */
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private ExtractorOutput extractorOutput; private final Id3Peeker id3Peeker;
private TrackOutput trackOutput; private final boolean isId3MetadataDisabled;
private FlacDecoderJni decoderJni; private FlacDecoderJni decoderJni;
private boolean metadataParsed; private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
private ParsableByteArray outputBuffer; private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer; private ByteBuffer outputByteBuffer;
private Metadata id3Metadata;
private boolean metadataParsed;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
this(0);
}
/**
* Constructs an instance.
*
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
id3Peeker = new Id3Peeker();
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
extractorOutput = output; extractorOutput = output;
...@@ -81,14 +122,19 @@ public final class FlacExtractor implements Extractor { ...@@ -81,14 +122,19 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length]; if (input.getPosition() == 0) {
input.peekFully(header, 0, FLAC_SIGNATURE.length); id3Metadata = peekId3Data(input);
return Arrays.equals(header, FLAC_SIGNATURE); }
return peekFlacSignature(input);
} }
@Override @Override
public int read(final ExtractorInput input, PositionHolder seekPosition) public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
}
decoderJni.setData(input); decoderJni.setData(input);
if (!metadataParsed) { if (!metadataParsed) {
...@@ -112,18 +158,21 @@ public final class FlacExtractor implements Extractor { ...@@ -112,18 +158,21 @@ public final class FlacExtractor implements Extractor {
: new SeekMap.Unseekable(streamInfo.durationUs(), 0)); : new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat = Format mediaFormat =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
null, /* id= */ null,
MimeTypes.AUDIO_RAW, MimeTypes.AUDIO_RAW,
null, /* codecs= */ null,
streamInfo.bitRate(), streamInfo.bitRate(),
streamInfo.maxDecodedFrameSize(), streamInfo.maxDecodedFrameSize(),
streamInfo.channels, streamInfo.channels,
streamInfo.sampleRate, streamInfo.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample), getPcmEncoding(streamInfo.bitsPerSample),
null, /* encoderDelay= */ 0,
null, /* encoderPadding= */ 0,
0, /* initializationData= */ null,
null); /* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat); trackOutput.format(mediaFormat);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
...@@ -170,6 +219,31 @@ public final class FlacExtractor implements Extractor { ...@@ -170,6 +219,31 @@ public final class FlacExtractor implements Extractor {
} }
} }
/**
* Peeks ID3 tag data (if present) at the beginning of the input.
*
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
* present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate =
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
/**
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
*
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
*/
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
input.peekFully(header, 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
}
private static final class FlacSeekMap implements SeekMap { private static final class FlacSeekMap implements SeekMap {
private final long durationUs; private final long durationUs;
......
...@@ -319,6 +319,8 @@ bool FLACParser::decodeMetadata() { ...@@ -319,6 +319,8 @@ bool FLACParser::decodeMetadata() {
case 48000: case 48000:
case 88200: case 88200:
case 96000: case 96000:
case 176400:
case 192000:
break; break;
default: default:
ALOGE("unsupported sample rate %u", getSampleRate()); ALOGE("unsupported sample rate %u", getSampleRate());
......
...@@ -26,6 +26,7 @@ android { ...@@ -26,6 +26,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'com.google.vr:sdk-audio:1.80.0' implementation 'com.google.vr:sdk-audio:1.80.0'
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.gvr; package com.google.android.exoplayer2.ext.gvr;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -39,7 +40,7 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -39,7 +40,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
private int sampleRateHz; private int sampleRateHz;
private int channelCount; private int channelCount;
private GvrAudioSurround gvrAudioSurround; @Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer; private ByteBuffer buffer;
private boolean inputEnded; private boolean inputEnded;
...@@ -48,14 +49,13 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -48,14 +49,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
private float y; private float y;
private float z; private float z;
/** /** Creates a new GVR audio processor. */
* Creates a new GVR audio processor.
*/
public GvrAudioProcessor() { public GvrAudioProcessor() {
// Use the identity for the initial orientation. // Use the identity for the initial orientation.
w = 1f; w = 1f;
sampleRateHz = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE; channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
} }
/** /**
...@@ -77,9 +77,11 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -77,9 +77,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
} }
} }
@SuppressWarnings("ReferenceEquality")
@Override @Override
public synchronized boolean configure(int sampleRateHz, int channelCount, public synchronized boolean configure(
@C.Encoding int encoding) throws UnhandledFormatException { int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_16BIT) { if (encoding != C.ENCODING_PCM_16BIT) {
maybeReleaseGvrAudioSurround(); maybeReleaseGvrAudioSurround();
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
...@@ -116,7 +118,7 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -116,7 +118,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount, gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
FRAMES_PER_OUTPUT_BUFFER); FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z); gvrAudioSurround.updateNativeOrientation(w, x, y, z);
if (buffer == null) { if (buffer == EMPTY_BUFFER) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder()); .order(ByteOrder.nativeOrder());
} }
...@@ -179,10 +181,11 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -179,10 +181,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override @Override
public synchronized void reset() { public synchronized void reset() {
maybeReleaseGvrAudioSurround(); maybeReleaseGvrAudioSurround();
updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
inputEnded = false; inputEnded = false;
buffer = null;
sampleRateHz = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE; channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
} }
private void maybeReleaseGvrAudioSurround() { private void maybeReleaseGvrAudioSurround() {
......
...@@ -29,12 +29,12 @@ dependencies { ...@@ -29,12 +29,12 @@ dependencies {
// This dependency is necessary to force the supportLibraryVersion of // This dependency is necessary to force the supportLibraryVersion of
// com.android.support:support-v4 to be used. Else an older version (25.2.0) // com.android.support:support-v4 to be used. Else an older version (25.2.0)
// is included via: // is included via:
// com.google.android.gms:play-services-ads:11.4.2 // com.google.android.gms:play-services-ads:12.0.0
// |-- com.google.android.gms:play-services-ads-lite:11.4.2 // |-- com.google.android.gms:play-services-ads-lite:12.0.0
// |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.google.android.gms:play-services-basement:12.0.0
// |-- com.android.support:support-v4:25.2.0 // |-- com.android.support:support-v4:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
} }
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.ext.ima"> package="com.google.android.exoplayer2.ext.ima">
<meta-data android:name="com.google.android.gms.version" <meta-data android:name="com.google.android.gms.version"
......
...@@ -20,6 +20,7 @@ import android.support.annotation.Nullable; ...@@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
...@@ -33,10 +34,12 @@ import java.io.IOException; ...@@ -33,10 +34,12 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/ */
@Deprecated @Deprecated
public final class ImaAdsMediaSource implements MediaSource { public final class ImaAdsMediaSource extends BaseMediaSource {
private final AdsMediaSource adsMediaSource; private final AdsMediaSource adsMediaSource;
private SourceInfoRefreshListener adsMediaSourceListener;
/** /**
* Constructs a new source that inserts ads linearly with the content specified by * Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}. * {@code contentMediaSource}.
...@@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource { ...@@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource {
} }
@Override @Override
public void prepareSource( public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
final ExoPlayer player, boolean isTopLevelSource, final Listener listener) { adsMediaSourceListener =
adsMediaSource.prepareSource( new SourceInfoRefreshListener() {
player,
isTopLevelSource,
new Listener() {
@Override @Override
public void onSourceInfoRefreshed( public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) { MediaSource source, Timeline timeline, @Nullable Object manifest) {
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest); refreshSourceInfo(timeline, manifest);
} }
}); };
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
} }
@Override @Override
...@@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource { ...@@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource {
} }
@Override @Override
public void releaseSource() { public void releaseSourceInternal() {
adsMediaSource.releaseSource(); adsMediaSource.releaseSource(adsMediaSourceListener);
} }
} }
# ExoPlayer Firebase JobDispatcher extension #
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
The easiest way to use the extension is to add it as a gradle dependency:
```gradle
implementation 'com.google.android.exoplayer:extension-jobdispatcher:2.X.X'
```
where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module
locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.firebase:firebase-jobdispatcher:0.8.5'
}
ext {
javadocTitle = 'Firebase JobDispatcher extension'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'extension-jobdispatcher'
releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.'
}
apply from: '../../publish.gradle'
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.jobdispatcher"/>
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.jobdispatcher;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.firebase.jobdispatcher.Constraint;
import com.firebase.jobdispatcher.FirebaseJobDispatcher;
import com.firebase.jobdispatcher.GooglePlayDriver;
import com.firebase.jobdispatcher.Job;
import com.firebase.jobdispatcher.Job.Builder;
import com.firebase.jobdispatcher.JobParameters;
import com.firebase.jobdispatcher.JobService;
import com.firebase.jobdispatcher.Lifetime;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Util;
/**
* A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
* {@link JobDispatcherSchedulerService} to your manifest:
*
* <pre>{@literal
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
*
* <service
* android:name="com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"
* android:exported="false">
* <intent-filter>
* <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
* </intent-filter>
* </service>
* }</pre>
*
* <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses
* should be guarded with a call to {@code
* GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
*
* @see <a
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
*/
public final class JobDispatcherScheduler implements Scheduler {
private static final String TAG = "JobDispatcherScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String KEY_REQUIREMENTS = "requirements";
private final String jobTag;
private final FirebaseJobDispatcher jobDispatcher;
/**
* @param context A context.
* @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous
* instance, anything scheduled by the previous instance will be canceled by this instance if
* {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
*/
public JobDispatcherScheduler(Context context, String jobTag) {
this.jobDispatcher =
new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
this.jobTag = jobTag;
}
@Override
public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
int result = jobDispatcher.schedule(job);
logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
}
@Override
public boolean cancel() {
int result = jobDispatcher.cancel(jobTag);
logd("Canceling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
private static Job buildJob(
FirebaseJobDispatcher dispatcher,
Requirements requirements,
String tag,
String serviceAction,
String servicePackage) {
Builder builder =
dispatcher
.newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
.setTag(tag);
switch (requirements.getRequiredNetworkType()) {
case Requirements.NETWORK_TYPE_NONE:
// do nothing.
break;
case Requirements.NETWORK_TYPE_ANY:
builder.addConstraint(Constraint.ON_ANY_NETWORK);
break;
case Requirements.NETWORK_TYPE_UNMETERED:
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
break;
default:
throw new UnsupportedOperationException();
}
if (requirements.isIdleRequired()) {
builder.addConstraint(Constraint.DEVICE_IDLE);
}
if (requirements.isChargingRequired()) {
builder.addConstraint(Constraint.DEVICE_CHARGING);
}
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
Bundle extras = new Bundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
builder.setExtras(extras);
return builder.build();
}
private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
}
}
/** A {@link JobService} that starts the target service if the requirements are met. */
public static final class JobDispatcherSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras();
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) {
logd("Requirements are met");
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(this, intent);
} else {
logd("Requirements are not met");
jobFinished(params, /* needsReschedule */ true);
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
}
...@@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -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 { ...@@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
* @param errorMessageProvider The {@link ErrorMessageProvider}. * @param errorMessageProvider The {@link ErrorMessageProvider}.
*/ */
public void setErrorMessageProvider( public void setErrorMessageProvider(
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) { @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider; this.errorMessageProvider = errorMessageProvider;
} }
......
...@@ -334,12 +334,11 @@ public final class MediaSessionConnector { ...@@ -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 { ...@@ -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 { ...@@ -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) {
this.errorMessageProvider = errorMessageProvider; if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
updateMediaSessionPlaybackState();
}
} }
/** /**
...@@ -451,9 +455,11 @@ public final class MediaSessionConnector { ...@@ -451,9 +455,11 @@ public final class MediaSessionConnector {
* @param queueNavigator The queue navigator. * @param queueNavigator The queue navigator.
*/ */
public void setQueueNavigator(QueueNavigator queueNavigator) { public void setQueueNavigator(QueueNavigator queueNavigator) {
unregisterCommandReceiver(this.queueNavigator); if (this.queueNavigator != queueNavigator) {
this.queueNavigator = queueNavigator; unregisterCommandReceiver(this.queueNavigator);
registerCommandReceiver(queueNavigator); this.queueNavigator = queueNavigator;
registerCommandReceiver(queueNavigator);
}
} }
/** /**
...@@ -462,11 +468,13 @@ public final class MediaSessionConnector { ...@@ -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) {
unregisterCommandReceiver(this.queueEditor); if (this.queueEditor != queueEditor) {
this.queueEditor = queueEditor; unregisterCommandReceiver(this.queueEditor);
registerCommandReceiver(queueEditor); this.queueEditor = queueEditor;
mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS registerCommandReceiver(queueEditor);
: EDITOR_MEDIA_SESSION_FLAGS); mediaSession.setFlags(
queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
}
} }
/** /**
...@@ -475,9 +483,11 @@ public final class MediaSessionConnector { ...@@ -475,9 +483,11 @@ public final class MediaSessionConnector {
* @param ratingCallback The rating callback. * @param ratingCallback The rating callback.
*/ */
public void setRatingCallback(RatingCallback ratingCallback) { public void setRatingCallback(RatingCallback ratingCallback) {
unregisterCommandReceiver(this.ratingCallback); if (this.ratingCallback != ratingCallback) {
this.ratingCallback = ratingCallback; unregisterCommandReceiver(this.ratingCallback);
registerCommandReceiver(this.ratingCallback); this.ratingCallback = ratingCallback;
registerCommandReceiver(this.ratingCallback);
}
} }
private void registerCommandReceiver(CommandReceiver commandReceiver) { private void registerCommandReceiver(CommandReceiver commandReceiver) {
...@@ -514,16 +524,16 @@ public final class MediaSessionConnector { ...@@ -514,16 +524,16 @@ public final class MediaSessionConnector {
} }
customActionMap = Collections.unmodifiableMap(currentActions); customActionMap = Collections.unmodifiableMap(currentActions);
int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR int playbackState = player.getPlaybackState();
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); ExoPlaybackException playbackError =
if (playbackException != null) { playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
if (errorMessageProvider != null) { int sessionPlaybackState =
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackException); playbackError != null
builder.setErrorMessage(message.first, message.second); ? PlaybackStateCompat.STATE_ERROR
} : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
if (player.getPlaybackState() != Player.STATE_IDLE) { if (playbackError != null && errorMessageProvider != null) {
playbackException = null; Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
} builder.setErrorMessage(message.first, message.second);
} }
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID; : MediaSessionCompat.QueueItem.UNKNOWN_ID;
...@@ -674,12 +684,8 @@ public final class MediaSessionConnector { ...@@ -674,12 +684,8 @@ public final class MediaSessionConnector {
// active queue item and queue navigation actions may need to be updated // active queue item and queue navigation actions may need to be updated
updateMediaSessionPlaybackState(); updateMediaSessionPlaybackState();
} }
if (currentWindowCount != windowCount) {
// active queue item and queue navigation actions may need to be updated
updateMediaSessionPlaybackState();
}
currentWindowCount = windowCount; currentWindowCount = windowCount;
currentWindowIndex = player.getCurrentWindowIndex(); currentWindowIndex = windowIndex;
updateMediaSessionMetadata(); updateMediaSessionMetadata();
} }
...@@ -704,12 +710,6 @@ public final class MediaSessionConnector { ...@@ -704,12 +710,6 @@ public final class MediaSessionConnector {
} }
@Override @Override
public void onPlayerError(ExoPlaybackException error) {
playbackException = error;
updateMediaSessionPlaybackState();
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (currentWindowIndex != player.getCurrentWindowIndex()) { if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) { if (queueNavigator != null) {
......
...@@ -24,21 +24,21 @@ import android.support.v4.media.session.MediaControllerCompat; ...@@ -24,21 +24,21 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.List; import java.util.List;
/** /**
* A {@link MediaSessionConnector.QueueEditor} implementation based on the * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
* {@link DynamicConcatenatingMediaSource}. * ConcatenatingMediaSource}.
* <p> *
* This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles * <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it. * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
* This allows to move the currently playing window without interrupting playback. * This allows to move the currently playing window without interrupting playback.
*/ */
public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor, public final class TimelineQueueEditor
MediaSessionConnector.CommandReceiver { implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver {
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window"; public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
public static final String EXTRA_FROM_INDEX = "from_index"; public static final String EXTRA_FROM_INDEX = "from_index";
...@@ -124,20 +124,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi ...@@ -124,20 +124,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
private final QueueDataAdapter queueDataAdapter; private final QueueDataAdapter queueDataAdapter;
private final MediaSourceFactory sourceFactory; private final MediaSourceFactory sourceFactory;
private final MediaDescriptionEqualityChecker equalityChecker; private final MediaDescriptionEqualityChecker equalityChecker;
private final DynamicConcatenatingMediaSource queueMediaSource; private final ConcatenatingMediaSource queueMediaSource;
/** /**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
* *
* @param mediaController A {@link MediaControllerCompat} to read the current queue. * @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources. * @param sourceFactory The {@link MediaSourceFactory} to build media sources.
*/ */
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, public TimelineQueueEditor(
@NonNull DynamicConcatenatingMediaSource queueMediaSource, @NonNull MediaControllerCompat mediaController,
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) { @NonNull ConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter,
@NonNull MediaSourceFactory sourceFactory) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
new MediaIdEqualityChecker()); new MediaIdEqualityChecker());
} }
...@@ -146,15 +147,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi ...@@ -146,15 +147,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
* *
* @param mediaController A {@link MediaControllerCompat} to read the current queue. * @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources. * @param sourceFactory The {@link MediaSourceFactory} to build media sources.
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items. * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/ */
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, public TimelineQueueEditor(
@NonNull DynamicConcatenatingMediaSource queueMediaSource, @NonNull MediaControllerCompat mediaController,
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory, @NonNull ConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter,
@NonNull MediaSourceFactory sourceFactory,
@NonNull MediaDescriptionEqualityChecker equalityChecker) { @NonNull MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController; this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource; this.queueMediaSource = queueMediaSource;
......
...@@ -73,10 +73,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu ...@@ -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 ...@@ -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;
......
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Herhaal niks</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Herhaal een</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Herhaal alles</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Herhaal niks"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Herhaal een"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Herhaal alles"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">ምንም አትድገም</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">አንድ ድገም</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">ሁሉንም ድገም</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"ምንም አትድገም"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"አንድ ድገም"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"ሁሉንም ድገም"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">عدم التكرار</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">تكرار مقطع صوتي واحد</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">تكرار الكل</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"عدم التكرار"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"تكرار مقطع صوتي واحد"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"تكرار الكل"</string>
</resources> </resources>
<?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>
<?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>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Ne ponavljaj nijednu</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Ponovi jednu</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Ponovi sve</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne ponavljaj nijednu"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ponovi jednu"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ponovi sve"</string>
</resources> </resources>
<?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>
<?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>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Без повтаряне</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Повтаряне на един елемент</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Повтаряне на всички</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Без повтаряне"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Повтаряне на един елемент"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Повтаряне на всички"</string>
</resources> </resources>
<?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>
<?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>
<?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>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">No en repeteixis cap</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Repeteix una</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Repeteix tot</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No en repeteixis cap"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeteix una"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeteix tot"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Neopakovat</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Opakovat jednu</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Opakovat vše</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Neopakovat"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Opakovat jednu"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Opakovat vše"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Gentag ingen</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Gentag én</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Gentag alle</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Gentag ingen"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Gentag én"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Gentag alle"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Keinen wiederholen</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Einen wiederholen</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Alle wiederholen</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Keinen wiederholen"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Einen wiederholen"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Alle wiederholen"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Καμία επανάληψη</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Επανάληψη ενός κομματιού</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Επανάληψη όλων</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Καμία επανάληψη"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Επανάληψη ενός κομματιού"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Επανάληψη όλων"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Repeat none</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Repeat one</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Repeat all</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Repeat none</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Repeat one</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Repeat all</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Repeat none</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Repeat one</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Repeat all</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">No repetir</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Repetir uno</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Repetir todo</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No repetir"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repetir uno"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repetir todo"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">No repetir</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Repetir uno</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Repetir todo</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No repetir"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repetir uno"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repetir todo"</string>
</resources> </resources>
<?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>
<?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>
<?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>
<?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>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">تکرار هیچ‌کدام</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">یکبار تکرار</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">تکرار همه</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"تکرار هیچ‌کدام"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"یکبار تکرار"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"تکرار همه"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Ei uudelleentoistoa</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Toista yksi uudelleen</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Toista kaikki uudelleen</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ei uudelleentoistoa"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Toista yksi uudelleen"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Toista kaikki uudelleen"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Lire une chanson en boucle</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Lire une chanson en boucle"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Tout lire en boucle"</string>
</resources> </resources>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project <resources>
<string name="exo_media_action_repeat_off_description">Ne rien lire en boucle</string>
Licensed under the Apache License, Version 2.0 (the "License"); <string name="exo_media_action_repeat_one_description">Lire un titre en boucle</string>
you may not use this file except in compliance with the License. <string name="exo_media_action_repeat_all_description">Tout lire en boucle</string>
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Lire un titre en boucle"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Tout lire en boucle"</string>
</resources> </resources>
<?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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment