Commit db53606c by Jinming he

Merge master into bugfix/format.

parents b3b878da 05a31dfd
Showing with 1321 additions and 750 deletions
...@@ -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.2'
releaseVersionCode = 2703 releaseVersionCode = 2802
// 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,15 @@ project.ext { ...@@ -25,12 +25,15 @@ 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 = '15.0.1'
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'
testRunnerVersion = '1.1.0-alpha3'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
......
...@@ -23,8 +23,8 @@ import com.google.android.exoplayer2.C; ...@@ -23,8 +23,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
...@@ -51,11 +51,9 @@ import com.google.android.gms.cast.MediaQueueItem; ...@@ -51,11 +51,9 @@ import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList; import java.util.ArrayList;
/** /** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
* Manages players and an internal media queue for the ExoPlayer/Cast demo app. /* package */ final class PlayerManager
*/ implements EventListener, CastPlayer.SessionAvailabilityListener {
/* package */ final class PlayerManager extends DefaultEventListener
implements CastPlayer.SessionAvailabilityListener {
/** /**
* Listener for changes in the media queue playback position. * Listener for changes in the media queue playback position.
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
......
...@@ -18,7 +18,10 @@ ...@@ -18,7 +18,10 @@
package="com.google.android.exoplayer2.demo"> package="com.google.android.exoplayer2.demo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<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 +76,18 @@ ...@@ -73,6 +76,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,6 +16,8 @@ ...@@ -16,6 +16,8 @@
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.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
...@@ -35,10 +37,17 @@ import java.io.File; ...@@ -35,10 +37,17 @@ import java.io.File;
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String DOWNLOAD_CACHE_FOLDER = "downloads"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
protected String userAgent; protected String userAgent;
private File downloadDirectory;
private Cache downloadCache; private Cache downloadCache;
private DownloadManager downloadManager;
private DownloadTracker downloadTracker;
@Override @Override
public void onCreate() { public void onCreate() {
...@@ -50,7 +59,7 @@ public class DemoApplication extends Application { ...@@ -50,7 +59,7 @@ public class DemoApplication extends Application {
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) { public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
DefaultDataSourceFactory upstreamFactory = DefaultDataSourceFactory upstreamFactory =
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
return createReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
} }
/** Returns a {@link HttpDataSource.Factory}. */ /** Returns a {@link HttpDataSource.Factory}. */
...@@ -59,31 +68,67 @@ public class DemoApplication extends Application { ...@@ -59,31 +68,67 @@ public class DemoApplication extends Application {
return new DefaultHttpDataSourceFactory(userAgent, listener); return new DefaultHttpDataSourceFactory(userAgent, listener);
} }
/** Returns the download {@link Cache}. */ /** Returns whether extension renderers should be used. */
public Cache getDownloadCache() { public boolean useExtensionRenderers() {
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));
downloadTracker =
new DownloadTracker(
/* context= */ this,
buildDataSourceFactory(/* listener= */ null),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker);
}
}
private synchronized Cache getDownloadCache() {
if (downloadCache == null) { if (downloadCache == null) {
File dir = getExternalFilesDir(null); File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
if (dir == null) { downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
dir = getFilesDir();
}
File downloadCacheFolder = new File(dir, DOWNLOAD_CACHE_FOLDER);
downloadCache = new SimpleCache(downloadCacheFolder, new NoOpCacheEvictor());
} }
return downloadCache; return downloadCache;
} }
public boolean useExtensionRenderers() { private File getDownloadDirectory() {
return "withExtensions".equals(BuildConfig.FLAVOR); if (downloadDirectory == null) {
downloadDirectory = getExternalFilesDir(null);
if (downloadDirectory == null) {
downloadDirectory = getFilesDir();
}
}
return downloadDirectory;
} }
private static CacheDataSourceFactory createReadOnlyCacheDataSource( private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) { DefaultDataSourceFactory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory( return new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,
new FileDataSourceFactory(), new FileDataSourceFactory(),
/*cacheWriteDataSinkFactory=*/ null, /* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/*eventListener=*/ null); /* eventListener= */ null);
} }
} }
/*
* 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() {}
}
<?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">"Ponovite sve"</string> android:id="@+id/representation_list"
<string name="exo_media_action_repeat_off_description">"Ne ponavljaju"</string> android:layout_width="match_parent"
<string name="exo_media_action_repeat_one_description">"Ponovite jedan"</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>
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
...@@ -26,16 +31,6 @@ android { ...@@ -26,16 +31,6 @@ android {
} }
dependencies { dependencies {
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example:
// com.google.android.gms:play-services-cast-framework:11.4.2
// |-- com.google.android.gms:play-services-basement:11.4.2
// |-- com.android.support:support-v4:25.2.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
...@@ -44,6 +39,15 @@ dependencies { ...@@ -44,6 +39,15 @@ dependencies {
testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example via:
// com.google.android.gms:play-services-cast-framework:15.0.1
// |-- com.android.support:mediarouter-v7:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
} }
ext { ext {
......
...@@ -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;
...@@ -283,6 +284,11 @@ public final class CastPlayer implements Player { ...@@ -283,6 +284,11 @@ public final class CastPlayer implements Player {
// Player implementation. // Player implementation.
@Override @Override
public AudioComponent getAudioComponent() {
return null;
}
@Override
public VideoComponent getVideoComponent() { public VideoComponent getVideoComponent() {
return null; return null;
} }
...@@ -308,6 +314,11 @@ public final class CastPlayer implements Player { ...@@ -308,6 +314,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 +492,14 @@ public final class CastPlayer implements Player { ...@@ -481,6 +492,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
...@@ -513,6 +532,15 @@ public final class CastPlayer implements Player { ...@@ -513,6 +532,15 @@ public final class CastPlayer implements Player {
} }
@Override @Override
public long getTotalBufferedDuration() {
long bufferedPosition = getBufferedPosition();
long currentPosition = getCurrentPosition();
return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET
? 0
: bufferedPosition - currentPosition;
}
@Override
public boolean isCurrentWindowDynamic() { public boolean isCurrentWindowDynamic() {
return !currentTimeline.isEmpty() return !currentTimeline.isEmpty()
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic; && currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
...@@ -549,6 +577,11 @@ public final class CastPlayer implements Player { ...@@ -549,6 +577,11 @@ public final class CastPlayer implements Player {
return getCurrentPosition(); return getCurrentPosition();
} }
@Override
public long getContentBufferedPosition() {
return getBufferedPosition();
}
// Internal methods. // Internal methods.
public void updateInternalState() { public void updateInternalState() {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.Nullable;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
...@@ -73,12 +74,22 @@ import java.util.Map; ...@@ -73,12 +74,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
...@@ -100,7 +111,7 @@ import java.util.Map; ...@@ -100,7 +111,7 @@ import java.util.Map;
// equals and hashCode implementations. // equals and hashCode implementations.
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (this == other) { if (this == other) {
return true; return true;
} else if (!(other instanceof CastTimeline)) { } else if (!(other instanceof CastTimeline)) {
......
...@@ -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);
} }
} }
...@@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack; ...@@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack;
* @return The equivalent {@link Format}. * @return The equivalent {@link Format}.
*/ */
public static Format mediaTrackToFormat(MediaTrack mediaTrack) { public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(), return Format.createContainerFormat(
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage()); mediaTrack.getContentId(),
/* label= */ null,
mediaTrack.getContentType(),
/* sampleMimeType= */ null,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0,
mediaTrack.getLanguage());
} }
private CastUtils() {} private CastUtils() {}
......
...@@ -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>
...@@ -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>
...@@ -70,7 +70,8 @@ COMMON_OPTIONS="\ ...@@ -70,7 +70,8 @@ COMMON_OPTIONS="\
--enable-decoder=flac \ --enable-decoder=flac \
" && \ " && \
cd "${FFMPEG_EXT_PATH}/jni" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && \
./configure \ ./configure \
--libdir=android-libs/armeabi-v7a \ --libdir=android-libs/armeabi-v7a \
--arch=arm \ --arch=arm \
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
...@@ -32,6 +37,8 @@ android { ...@@ -32,6 +37,8 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }
ext { ext {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler; 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.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -26,7 +27,10 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink; ...@@ -26,7 +27,10 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Collections;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Decodes and renders audio using FFmpeg. * Decodes and renders audio using FFmpeg.
...@@ -45,10 +49,10 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -45,10 +49,10 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
private final boolean enableFloatOutput; private final boolean enableFloatOutput;
private FfmpegDecoder decoder; private @MonotonicNonNull FfmpegDecoder decoder;
public FfmpegAudioRenderer() { public FfmpegAudioRenderer() {
this(null, null); this(/* eventHandler= */ null, /* eventListener= */ null);
} }
/** /**
...@@ -57,9 +61,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -57,9 +61,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/ */
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) { AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false); this(
eventHandler,
eventListener,
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
/* enableFloatOutput= */ false);
} }
/** /**
...@@ -72,19 +82,28 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -72,19 +82,28 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch * 32-bit float output, any audio processing will be disabled, including playback speed/pitch
* adjustment. * adjustment.
*/ */
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(
AudioSink audioSink, boolean enableFloatOutput) { @Nullable Handler eventHandler,
super(eventHandler, eventListener, null, false, audioSink); @Nullable AudioRendererEventListener eventListener,
AudioSink audioSink,
boolean enableFloatOutput) {
super(
eventHandler,
eventListener,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
audioSink);
this.enableFloatOutput = enableFloatOutput; this.enableFloatOutput = enableFloatOutput;
} }
@Override @Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager, protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
Format format) { Format format) {
String sampleMimeType = format.sampleMimeType; Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) { } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
|| !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
...@@ -101,18 +120,35 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -101,18 +120,35 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
@Override @Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException { throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, decoder =
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format)); new FfmpegDecoder(
NUM_BUFFERS,
NUM_BUFFERS,
INITIAL_INPUT_BUFFER_SIZE,
format,
shouldUseFloatOutput(format));
return decoder; return decoder;
} }
@Override @Override
public Format getOutputFormat() { public Format getOutputFormat() {
Assertions.checkNotNull(decoder);
int channelCount = decoder.getChannelCount(); int channelCount = decoder.getChannelCount();
int sampleRate = decoder.getSampleRate(); int sampleRate = decoder.getSampleRate();
@C.PcmEncoding int encoding = decoder.getEncoding(); @C.PcmEncoding int encoding = decoder.getEncoding();
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, return Format.createAudioSampleFormat(
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null); /* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
Format.NO_VALUE,
Format.NO_VALUE,
channelCount,
sampleRate,
encoding,
Collections.emptyList(),
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
} }
private boolean isOutputSupported(Format inputFormat) { private boolean isOutputSupported(Format inputFormat) {
...@@ -120,6 +156,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -120,6 +156,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
private boolean shouldUseFloatOutput(Format inputFormat) { private boolean shouldUseFloatOutput(Format inputFormat) {
Assertions.checkNotNull(inputFormat.sampleMimeType);
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) { if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
return false; return false;
} }
......
...@@ -15,10 +15,13 @@ ...@@ -15,10 +15,13 @@
*/ */
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
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.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -30,13 +33,12 @@ import java.util.List; ...@@ -30,13 +33,12 @@ import java.util.List;
/* package */ final class FfmpegDecoder extends /* package */ final class FfmpegDecoder extends
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> { SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio. // Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2; private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
// Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
private final String codecName; private final String codecName;
private final byte[] extraData; private final @Nullable byte[] extraData;
private final @C.Encoding int encoding; private final @C.Encoding int encoding;
private final int outputBufferSize; private final int outputBufferSize;
...@@ -45,18 +47,26 @@ import java.util.List; ...@@ -45,18 +47,26 @@ import java.util.List;
private volatile int channelCount; private volatile int channelCount;
private volatile int sampleRate; private volatile int sampleRate;
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, public FfmpegDecoder(
String mimeType, List<byte[]> initializationData, boolean outputFloat) int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
Format format,
boolean outputFloat)
throws FfmpegDecoderException { throws FfmpegDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
throw new FfmpegDecoderException("Failed to load decoder native libraries."); throw new FfmpegDecoderException("Failed to load decoder native libraries.");
} }
codecName = FfmpegLibrary.getCodecName(mimeType); Assertions.checkNotNull(format.sampleMimeType);
extraData = getExtraData(mimeType, initializationData); codecName =
Assertions.checkNotNull(
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
nativeContext = ffmpegInitialize(codecName, extraData, outputFloat); nativeContext =
ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);
if (nativeContext == 0) { if (nativeContext == 0) {
throw new FfmpegDecoderException("Initialization failed."); throw new FfmpegDecoderException("Initialization failed.");
} }
...@@ -84,7 +94,7 @@ import java.util.List; ...@@ -84,7 +94,7 @@ import java.util.List;
} }
@Override @Override
protected FfmpegDecoderException decode( protected @Nullable FfmpegDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) { if (reset) {
nativeContext = ffmpegReset(nativeContext, extraData); nativeContext = ffmpegReset(nativeContext, extraData);
...@@ -103,6 +113,7 @@ import java.util.List; ...@@ -103,6 +113,7 @@ import java.util.List;
channelCount = ffmpegGetChannelCount(nativeContext); channelCount = ffmpegGetChannelCount(nativeContext);
sampleRate = ffmpegGetSampleRate(nativeContext); sampleRate = ffmpegGetSampleRate(nativeContext);
if (sampleRate == 0 && "alac".equals(codecName)) { if (sampleRate == 0 && "alac".equals(codecName)) {
Assertions.checkNotNull(extraData);
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG. // ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
// See https://trac.ffmpeg.org/ticket/6096 // See https://trac.ffmpeg.org/ticket/6096
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData); ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
...@@ -148,7 +159,7 @@ import java.util.List; ...@@ -148,7 +159,7 @@ import java.util.List;
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
* not required. * not required.
*/ */
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) { private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_ALAC:
...@@ -173,12 +184,20 @@ import java.util.List; ...@@ -173,12 +184,20 @@ import java.util.List;
} }
} }
private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat); private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,
boolean outputFloat,
int rawSampleRate,
int rawChannelCount);
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize, private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
ByteBuffer outputData, int outputSize); ByteBuffer outputData, int outputSize);
private native int ffmpegGetChannelCount(long context); private native int ffmpegGetChannelCount(long context);
private native int ffmpegGetSampleRate(long context); private native int ffmpegGetSampleRate(long context);
private native long ffmpegReset(long context, byte[] extraData);
private native long ffmpegReset(long context, @Nullable byte[] extraData);
private native void ffmpegRelease(long context); private native void ffmpegRelease(long context);
} }
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -51,10 +53,8 @@ public final class FfmpegLibrary { ...@@ -51,10 +53,8 @@ public final class FfmpegLibrary {
return LOADER.isAvailable(); return LOADER.isAvailable();
} }
/** /** Returns the version of the underlying library if available, or null otherwise. */
* Returns the version of the underlying library if available, or null otherwise. public static @Nullable String getVersion() {
*/
public static String getVersion() {
return isAvailable() ? ffmpegGetVersion() : null; return isAvailable() ? ffmpegGetVersion() : null;
} }
...@@ -62,19 +62,21 @@ public final class FfmpegLibrary { ...@@ -62,19 +62,21 @@ public final class FfmpegLibrary {
* Returns whether the underlying library supports the specified MIME type. * Returns whether the underlying library supports the specified MIME type.
* *
* @param mimeType The MIME type to check. * @param mimeType The MIME type to check.
* @param encoding The PCM encoding for raw audio.
*/ */
public static boolean supportsFormat(String mimeType) { public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
if (!isAvailable()) { if (!isAvailable()) {
return false; return false;
} }
String codecName = getCodecName(mimeType); String codecName = getCodecName(mimeType, encoding);
return codecName != null && ffmpegHasDecoder(codecName); return codecName != null && ffmpegHasDecoder(codecName);
} }
/** /**
* Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}. * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
* if it's unsupported.
*/ */
/* package */ static String getCodecName(String mimeType) { /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
return "aac"; return "aac";
...@@ -103,6 +105,14 @@ public final class FfmpegLibrary { ...@@ -103,6 +105,14 @@ public final class FfmpegLibrary {
return "flac"; return "flac";
case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_ALAC:
return "alac"; return "alac";
case MimeTypes.AUDIO_RAW:
if (encoding == C.ENCODING_PCM_MU_LAW) {
return "pcm_mulaw";
} else if (encoding == C.ENCODING_PCM_A_LAW) {
return "pcm_alaw";
} else {
return null;
}
default: default:
return null; return null;
} }
......
...@@ -27,6 +27,7 @@ extern "C" { ...@@ -27,6 +27,7 @@ extern "C" {
#endif #endif
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#include <libavresample/avresample.h> #include <libavresample/avresample.h>
#include <libavutil/channel_layout.h>
#include <libavutil/error.h> #include <libavutil/error.h>
#include <libavutil/opt.h> #include <libavutil/opt.h>
} }
...@@ -72,8 +73,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName); ...@@ -72,8 +73,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
* provided extraData as initialization data for the decoder if it is non-NULL. * provided extraData as initialization data for the decoder if it is non-NULL.
* Returns the created context. * Returns the created context.
*/ */
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
jbyteArray extraData, jboolean outputFloat); jboolean outputFloat, jint rawSampleRate,
jint rawChannelCount);
/** /**
* Decodes the packet into the output buffer, returning the number of bytes * Decodes the packet into the output buffer, returning the number of bytes
...@@ -110,13 +112,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { ...@@ -110,13 +112,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
} }
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
jboolean outputFloat) { jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
AVCodec *codec = getCodecByName(env, codecName); AVCodec *codec = getCodecByName(env, codecName);
if (!codec) { if (!codec) {
LOGE("Codec not found."); LOGE("Codec not found.");
return 0L; return 0L;
} }
return (jlong) createContext(env, codec, extraData, outputFloat); return (jlong)createContext(env, codec, extraData, outputFloat, rawSampleRate,
rawChannelCount);
} }
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
...@@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { ...@@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
LOGE("Unexpected error finding codec %d.", codecId); LOGE("Unexpected error finding codec %d.", codecId);
return 0L; return 0L;
} }
return (jlong) createContext(env, codec, extraData, jboolean outputFloat =
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT); (jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
return (jlong)createContext(env, codec, extraData, outputFloat,
/* rawSampleRate= */ -1,
/* rawChannelCount= */ -1);
} }
avcodec_flush_buffers(context); avcodec_flush_buffers(context);
...@@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) { ...@@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
return codec; return codec;
} }
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
jbyteArray extraData, jboolean outputFloat) { jboolean outputFloat, jint rawSampleRate,
jint rawChannelCount) {
AVCodecContext *context = avcodec_alloc_context3(codec); AVCodecContext *context = avcodec_alloc_context3(codec);
if (!context) { if (!context) {
LOGE("Failed to allocate context."); LOGE("Failed to allocate context.");
...@@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, ...@@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
} }
env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata); env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata);
} }
if (context->codec_id == AV_CODEC_ID_PCM_MULAW ||
context->codec_id == AV_CODEC_ID_PCM_ALAW) {
context->sample_rate = rawSampleRate;
context->channels = rawChannelCount;
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
}
int result = avcodec_open2(context, codec, NULL); int result = avcodec_open2(context, codec, NULL);
if (result < 0) { if (result < 0) {
logError("avcodec_open2", result); logError("avcodec_open2", result);
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
...@@ -31,8 +36,10 @@ android { ...@@ -31,8 +36,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">
......
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
/*
* 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.flac;
import static com.google.common.truth.Truth.assertThat;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
/** Unit test for {@link FlacBinarySearchSeeker}. */
public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
@Override
protected void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.hasPendingSeek()).isTrue();
}
}
...@@ -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());
}
} }
...@@ -64,8 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { ...@@ -64,8 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
} }
} }
private static class TestPlaybackRunnable extends Player.DefaultEventListener private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
implements Runnable {
private final Context context; private final Context context;
private final Uri uri; private final Uri uri;
......
...@@ -92,18 +92,14 @@ import java.util.List; ...@@ -92,18 +92,14 @@ import java.util.List;
} }
decoderJni.setData(inputBuffer.data); decoderJni.setData(inputBuffer.data);
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
int result;
try { try {
result = decoderJni.decodeSample(outputData); decoderJni.decodeSample(outputData);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
return new FlacDecoderException("Frame decoding failed", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (result < 0) {
return new FlacDecoderException("Frame decoding failed");
}
outputData.position(0);
outputData.limit(result);
return null; return null;
} }
......
...@@ -26,6 +26,17 @@ import java.nio.ByteBuffer; ...@@ -26,6 +26,17 @@ import java.nio.ByteBuffer;
*/ */
/* package */ final class FlacDecoderJni { /* package */ final class FlacDecoderJni {
/** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */
public static final class FlacFrameDecodeException extends Exception {
public final int errorCode;
public FlacFrameDecodeException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
}
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private final long nativeDecoderContext; private final long nativeDecoderContext;
...@@ -116,14 +127,50 @@ import java.nio.ByteBuffer; ...@@ -116,14 +127,50 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); return flacDecodeMetadata(nativeDecoderContext);
} }
public int decodeSample(ByteBuffer output) throws IOException, InterruptedException { /**
return output.isDirect() * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
? flacDecodeToBuffer(nativeDecoderContext, output) * error occurs, resets the stream and input to the given {@code retryPosition}.
: flacDecodeToArray(nativeDecoderContext, output.array()); *
* @param output The byte buffer to hold the decoded frame.
* @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
*/
public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
throws InterruptedException, IOException, FlacFrameDecodeException {
try {
decodeSample(output);
} catch (IOException e) {
if (retryPosition >= 0) {
reset(retryPosition);
if (extractorInput != null) {
extractorInput.setRetryPosition(retryPosition, e);
}
}
throw e;
}
}
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
public void decodeSample(ByteBuffer output)
throws IOException, InterruptedException, FlacFrameDecodeException {
output.clear();
int frameSize =
output.isDirect()
? flacDecodeToBuffer(nativeDecoderContext, output)
: flacDecodeToArray(nativeDecoderContext, output.array());
if (frameSize < 0) {
if (!isDecoderAtEndOfInput()) {
throw new FlacFrameDecodeException("Cannot decode FLAC frame", frameSize);
}
// The decoder has read to EOI. Return a 0-size frame to indicate the EOI.
output.limit(0);
} else {
output.limit(frameSize);
}
} }
/** /**
...@@ -133,8 +180,19 @@ import java.nio.ByteBuffer; ...@@ -133,8 +180,19 @@ import java.nio.ByteBuffer;
return flacGetDecodePosition(nativeDecoderContext); return flacGetDecodePosition(nativeDecoderContext);
} }
public long getLastSampleTimestamp() { /** Returns the timestamp for the first sample in the last decoded frame. */
return flacGetLastTimestamp(nativeDecoderContext); public long getLastFrameTimestamp() {
return flacGetLastFrameTimestamp(nativeDecoderContext);
}
/** Returns the first sample index of the last extracted frame. */
public long getLastFrameFirstSampleIndex() {
return flacGetLastFrameFirstSampleIndex(nativeDecoderContext);
}
/** Returns the first sample index of the frame to be extracted next. */
public long getNextFrameFirstSampleIndex() {
return flacGetNextFrameFirstSampleIndex(nativeDecoderContext);
} }
/** /**
...@@ -153,6 +211,11 @@ import java.nio.ByteBuffer; ...@@ -153,6 +211,11 @@ import java.nio.ByteBuffer;
return flacGetStateString(nativeDecoderContext); return flacGetStateString(nativeDecoderContext);
} }
/** Returns whether the decoder has read to the end of the input. */
public boolean isDecoderAtEndOfInput() {
return flacIsDecoderAtEndOfStream(nativeDecoderContext);
}
public void flush() { public void flush() {
flacFlush(nativeDecoderContext); flacFlush(nativeDecoderContext);
} }
...@@ -181,18 +244,34 @@ import java.nio.ByteBuffer; ...@@ -181,18 +244,34 @@ import java.nio.ByteBuffer;
} }
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamInfo flacDecodeMetadata(long context)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToArray(long context, byte[] outputArray) private native int flacDecodeToArray(long context, byte[] outputArray)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native long flacGetDecodePosition(long context); private native long flacGetDecodePosition(long context);
private native long flacGetLastTimestamp(long context);
private native long flacGetLastFrameTimestamp(long context);
private native long flacGetLastFrameFirstSampleIndex(long context);
private native long flacGetNextFrameFirstSampleIndex(long context);
private native long flacGetSeekPosition(long context, long timeUs); private native long flacGetSeekPosition(long context, long timeUs);
private native String flacGetStateString(long context); private native String flacGetStateString(long context);
private native boolean flacIsDecoderAtEndOfStream(long context);
private native void flacFlush(long context); private native void flacFlush(long context);
private native void flacReset(long context, long newPosition); private native void flacReset(long context, long newPosition);
private native void flacRelease(long context); private native void flacRelease(long context);
} }
...@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) { ...@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {
return context->parser->getDecodePosition(); return context->parser->getDecodePosition();
} }
DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) { DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastTimestamp(); return context->parser->getLastFrameTimestamp();
}
DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastFrameFirstSampleIndex();
}
DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getNextFrameFirstSampleIndex();
} }
DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
...@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { ...@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
return env->NewStringUTF(str); return env->NewStringUTF(str);
} }
DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->isDecoderAtEndOfStream();
}
DECODER_FUNC(void, flacFlush, jlong jContext) { DECODER_FUNC(void, flacFlush, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
context->parser->flush(); context->parser->flush();
......
...@@ -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());
......
...@@ -44,10 +44,18 @@ class FLACParser { ...@@ -44,10 +44,18 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
int64_t getLastTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
} }
int64_t getLastFrameFirstSampleIndex() const {
return mWriteHeader.number.sample_number;
}
int64_t getNextFrameFirstSampleIndex() const {
return mWriteHeader.number.sample_number + mWriteHeader.blocksize;
}
bool decodeMetadata(); bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size); size_t readBuffer(void *output, size_t output_size);
...@@ -83,6 +91,11 @@ class FLACParser { ...@@ -83,6 +91,11 @@ class FLACParser {
return FLAC__stream_decoder_get_resolved_state_string(mDecoder); return FLAC__stream_decoder_get_resolved_state_string(mDecoder);
} }
bool isDecoderAtEndOfStream() const {
return FLAC__stream_decoder_get_state(mDecoder) ==
FLAC__STREAM_DECODER_END_OF_STREAM;
}
private: private:
DataSource *mDataSource; DataSource *mDataSource;
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; ...@@ -20,6 +20,7 @@ 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;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.util.Assertions;
import com.google.vr.sdk.audio.GvrAudioSurround; import com.google.vr.sdk.audio.GvrAudioSurround;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
...@@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override @Override
public void queueInput(ByteBuffer input) { public void queueInput(ByteBuffer input) {
int position = input.position(); int position = input.position();
Assertions.checkNotNull(gvrAudioSurround);
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position); int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
input.position(position + readBytes); input.position(position + readBytes);
} }
@Override @Override
public void queueEndOfStream() { public void queueEndOfStream() {
Assertions.checkNotNull(gvrAudioSurround);
inputEnded = true; inputEnded = true;
gvrAudioSurround.triggerProcessing(); gvrAudioSurround.triggerProcessing();
} }
@Override @Override
public ByteBuffer getOutput() { public ByteBuffer getOutput() {
Assertions.checkNotNull(gvrAudioSurround);
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity()); int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes); buffer.position(0).limit(writtenBytes);
return buffer; return buffer;
...@@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override @Override
public boolean isEnded() { public boolean isEnded() {
Assertions.checkNotNull(gvrAudioSurround);
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0; return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
} }
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
...@@ -26,17 +31,21 @@ android { ...@@ -26,17 +31,21 @@ android {
} }
dependencies { dependencies {
// This dependency is necessary to force the supportLibraryVersion of api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.7'
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
// is included via:
// com.google.android.gms:play-services-ads:11.4.2
// |-- com.google.android.gms:play-services-ads-lite:11.4.2
// |-- com.google.android.gms:play-services-basement:11.4.2
// |-- com.android.support:support-v4:25.2.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
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
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4 and com.android.support:customtabs to be
// used. Else older versions are used, for example via:
// com.google.android.gms:play-services-ads:15.0.1
// |-- com.android.support:customtabs:26.1.0
implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
ext { ext {
......
<?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"
......
...@@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Timeline; ...@@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BaseMediaSource; 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.MediaSource.SourceInfoRefreshListener;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -34,12 +36,10 @@ import java.io.IOException; ...@@ -34,12 +36,10 @@ 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 extends BaseMediaSource { public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
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}.
...@@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource { ...@@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
} }
@Override @Override
public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { public void prepareSourceInternal(
adsMediaSourceListener = final ExoPlayer player,
new SourceInfoRefreshListener() { boolean isTopLevelSource,
@Override @Nullable TransferListener<? super DataSource> mediaTransferListener) {
public void onSourceInfoRefreshed( adsMediaSource.prepareSource(
MediaSource source, Timeline timeline, @Nullable Object manifest) { player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
refreshSourceInfo(timeline, manifest);
}
};
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
} }
@Override @Override
...@@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource { ...@@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
@Override @Override
public void releaseSourceInternal() { public void releaseSourceInternal() {
adsMediaSource.releaseSource(adsMediaSourceListener); adsMediaSource.releaseSource(/* listener= */ this);
}
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
refreshSourceInfo(timeline, manifest);
} }
} }
...@@ -20,6 +20,11 @@ android { ...@@ -20,6 +20,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
......
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer2.ext.jobdispatcher; package com.google.android.exoplayer2.ext.jobdispatcher;
import android.app.Notification;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
...@@ -31,16 +29,12 @@ import com.firebase.jobdispatcher.JobService; ...@@ -31,16 +29,12 @@ import com.firebase.jobdispatcher.JobService;
import com.firebase.jobdispatcher.Lifetime; import com.firebase.jobdispatcher.Lifetime;
import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
* A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
* schedule a {@link Service} to be started when its requirements are met. The started service must * {@link JobDispatcherSchedulerService} to your manifest:
* call {@link Service#startForeground(int, Notification)} to make itself a foreground service upon
* being started, as documented by {@link Service#startForegroundService(Intent)}.
*
* <p>To use {@link JobDispatcherScheduler} application needs to have RECEIVE_BOOT_COMPLETED
* permission and you need to define JobDispatcherSchedulerService in your manifest:
* *
* <pre>{@literal * <pre>{@literal
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> * <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
...@@ -54,18 +48,6 @@ import com.google.android.exoplayer2.util.Util; ...@@ -54,18 +48,6 @@ import com.google.android.exoplayer2.util.Util;
* </service> * </service>
* }</pre> * }</pre>
* *
* The service to be scheduled must be defined in the manifest with an intent-filter:
*
* <pre>{@literal
* <service android:name="MyJobService"
* android:exported="false">
* <intent-filter>
* <action android:name="MyJobService.action"/>
* <category android:name="android.intent.category.DEFAULT"/>
* </intent-filter>
* </service>
* }</pre>
*
* <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses * <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses
* should be guarded with a call to {@code * should be guarded with a call to {@code
* GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)} * GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
...@@ -76,44 +58,37 @@ import com.google.android.exoplayer2.util.Util; ...@@ -76,44 +58,37 @@ import com.google.android.exoplayer2.util.Util;
public final class JobDispatcherScheduler implements Scheduler { public final class JobDispatcherScheduler implements Scheduler {
private static final String TAG = "JobDispatcherScheduler"; private static final String TAG = "JobDispatcherScheduler";
private static final String SERVICE_ACTION = "SERVICE_ACTION"; private static final String KEY_SERVICE_ACTION = "service_action";
private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE"; private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String REQUIREMENTS = "REQUIREMENTS"; private static final String KEY_REQUIREMENTS = "requirements";
private final String jobTag; private final String jobTag;
private final Job job;
private final FirebaseJobDispatcher jobDispatcher; private final FirebaseJobDispatcher jobDispatcher;
/** /**
* @param context Used to create a {@link FirebaseJobDispatcher} service. * @param context A context.
* @param requirements The requirements to execute the job. * @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous
* @param jobTag Unique tag for the job. Using the same tag as a previous job can cause that job * instance, anything scheduled by the previous instance will be canceled by this instance if
* to be replaced or canceled. * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
* @param serviceAction The action which the service will be started with.
* @param servicePackage The package of the service which contains the logic of the job.
*/ */
public JobDispatcherScheduler( public JobDispatcherScheduler(Context context, String jobTag) {
Context context, this.jobDispatcher =
Requirements requirements, new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
String jobTag,
String serviceAction,
String servicePackage) {
this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
this.jobTag = jobTag; this.jobTag = jobTag;
this.job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
} }
@Override @Override
public boolean schedule() { public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
int result = jobDispatcher.schedule(job); int result = jobDispatcher.schedule(job);
logd("Scheduling JobDispatcher job: " + jobTag + " result: " + result); logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
} }
@Override @Override
public boolean cancel() { public boolean cancel() {
int result = jobDispatcher.cancel(jobTag); int result = jobDispatcher.cancel(jobTag);
logd("Canceling JobDispatcher job: " + jobTag + " result: " + result); logd("Canceling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
} }
...@@ -151,13 +126,12 @@ public final class JobDispatcherScheduler implements Scheduler { ...@@ -151,13 +126,12 @@ public final class JobDispatcherScheduler implements Scheduler {
} }
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true); builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
// Extras, work duration.
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(SERVICE_ACTION, serviceAction); extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(SERVICE_PACKAGE, servicePackage); extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
extras.putInt(REQUIREMENTS, requirements.getRequirementsData()); extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
builder.setExtras(extras); builder.setExtras(extras);
return builder.build(); return builder.build();
} }
...@@ -167,26 +141,25 @@ public final class JobDispatcherScheduler implements Scheduler { ...@@ -167,26 +141,25 @@ public final class JobDispatcherScheduler implements Scheduler {
} }
} }
/** A {@link JobService} to start a service if the requirements are met. */ /** A {@link JobService} that starts the target service if the requirements are met. */
public static final class JobDispatcherSchedulerService extends JobService { public static final class JobDispatcherSchedulerService extends JobService {
@Override @Override
public boolean onStartJob(JobParameters params) { public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started"); logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras(); Bundle extras = params.getExtras();
Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS)); Assertions.checkNotNull(extras, "Service started without extras.");
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) { if (requirements.checkRequirements(this)) {
logd("requirements are met"); logd("Requirements are met");
String serviceAction = extras.getString(SERVICE_ACTION); String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(SERVICE_PACKAGE); String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Assertions.checkNotNull(serviceAction, "Service action missing.");
Assertions.checkNotNull(servicePackage, "Service package missing.");
Intent intent = new Intent(serviceAction).setPackage(servicePackage); Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("starting service action: " + serviceAction + " package: " + servicePackage); logd("Starting service action: " + serviceAction + " package: " + servicePackage);
if (Util.SDK_INT >= 26) { Util.startForegroundService(this, intent);
startForegroundService(intent);
} else {
startService(intent);
}
} else { } else {
logd("requirements are not met"); logd("Requirements are not met");
jobFinished(params, /* needsReschedule */ true); jobFinished(params, /* needsReschedule */ true);
} }
return false; return false;
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion 17 minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
......
...@@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider; ...@@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoListener;
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */ /** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
public final class LeanbackPlayerAdapter extends PlayerAdapter { public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {
static { static {
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback"); ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
...@@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private final Player player; private final Player player;
private final Handler handler; private final Handler handler;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private final Runnable updateProgressRunnable; private final int updatePeriodMs;
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 @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface; private boolean hasSurface;
private boolean lastNotifiedPreparedState; private boolean lastNotifiedPreparedState;
...@@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) { public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
this.context = context; this.context = context;
this.player = player; this.player = player;
this.updatePeriodMs = updatePeriodMs;
handler = new Handler(); handler = new Handler();
componentListener = new ComponentListener(); componentListener = new ComponentListener();
controlDispatcher = new DefaultControlDispatcher(); controlDispatcher = new DefaultControlDispatcher();
updateProgressRunnable = new Runnable() {
@Override
public void run() {
Callback callback = getCallback();
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
handler.postDelayed(this, updatePeriodMs);
}
};
} }
/** /**
...@@ -110,7 +102,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -110,7 +102,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;
} }
...@@ -138,7 +130,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -138,7 +130,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
videoComponent.removeVideoListener(componentListener); videoComponent.removeVideoListener(componentListener);
} }
if (surfaceHolderGlueHost != null) { if (surfaceHolderGlueHost != null) {
surfaceHolderGlueHost.setSurfaceHolderCallback(null); removeSurfaceHolderCallback(surfaceHolderGlueHost);
surfaceHolderGlueHost = null; surfaceHolderGlueHost = null;
} }
hasSurface = false; hasSurface = false;
...@@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override @Override
public void setProgressUpdatingEnabled(boolean enabled) { public void setProgressUpdatingEnabled(boolean enabled) {
handler.removeCallbacks(updateProgressRunnable); handler.removeCallbacks(this);
if (enabled) { if (enabled) {
handler.post(updateProgressRunnable); handler.post(this);
} }
} }
...@@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
&& (surfaceHolderGlueHost == null || hasSurface); && (surfaceHolderGlueHost == null || hasSurface);
} }
// Runnable implementation.
@Override
public void run() {
Callback callback = getCallback();
callback.onCurrentPositionChanged(this);
callback.onBufferedPositionChanged(this);
handler.postDelayed(this, updatePeriodMs);
}
// Internal methods. // Internal methods.
/* package */ void setVideoSurface(Surface surface) { /* package */ void setVideoSurface(@Nullable Surface surface) {
hasSurface = surface != null; hasSurface = surface != null;
Player.VideoComponent videoComponent = player.getVideoComponent(); Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) { if (videoComponent != null) {
...@@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
} }
} }
private final class ComponentListener extends Player.DefaultEventListener @SuppressWarnings("nullness:argument.type.incompatible")
implements SurfaceHolder.Callback, VideoListener { private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) {
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
}
private final class ComponentListener
implements Player.EventListener, SurfaceHolder.Callback, VideoListener {
// SurfaceHolder.Callback implementation. // SurfaceHolder.Callback implementation.
......
...@@ -18,6 +18,11 @@ android { ...@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
......
...@@ -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>
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