diff --git a/README.md b/README.md
index d7bc23f700..c67fb09d73 100644
--- a/README.md
+++ b/README.md
@@ -9,34 +9,39 @@ and extend, and can be updated through Play Store application updates.
## Documentation ##
-* The [developer guide][] provides a wealth of information to help you get
- started.
-* The [class reference][] documents the ExoPlayer library classes.
+* The [developer guide][] provides a wealth of information.
+* The [class reference][] documents ExoPlayer classes.
* The [release notes][] document the major changes in each release.
+* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
+ developments!
[developer guide]: https://google.github.io/ExoPlayer/guide.html
[class reference]: https://google.github.io/ExoPlayer/doc/reference
-[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md
+[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
+[developer blog]: https://medium.com/google-exoplayer
## Using ExoPlayer ##
-ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
+ExoPlayer modules can be obtained from JCenter. It's also possible to clone the
repository and depend on the modules locally.
-### Via jCenter ###
+### From JCenter ###
The easiest way to get started using ExoPlayer is to add it as a gradle
-dependency. You need to make sure you have the jcenter repository included in
-the `build.gradle` file in the root of your project:
+dependency. You need to make sure you have the JCenter and Google Maven
+repositories included in the `build.gradle` file in the root of your project:
```gradle
repositories {
jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
}
```
Next add a gradle compile dependency to the `build.gradle` file of your app
-module. The following will add a dependency to the full ExoPlayer library:
+module. The following will add a dependency to the full library:
```gradle
compile 'com.google.android.exoplayer:exoplayer:r2.X.X'
@@ -53,8 +58,8 @@ compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X'
compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X'
```
-The available modules are listed below. Adding a dependency to the full
-ExoPlayer library is equivalent to adding dependencies on all of the modules
+The available library modules are listed below. Adding a dependency to the full
+library is equivalent to adding dependencies on all of the library modules
individually.
* `exoplayer-core`: Core functionality (required).
@@ -63,11 +68,16 @@ individually.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
-For more details, see the project on [Bintray][]. For information about the
-latest versions, see the [Release notes][].
+In addition to library modules, ExoPlayer has multiple extension modules that
+depend on external libraries to provide additional functionality. Some
+extensions are available from JCenter, whereas others must be built manaully.
+Browse the [extensions directory] and their individual READMEs for details.
+More information on the library and extension modules that are available from
+JCenter can be found on [Bintray][].
+
+[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[Bintray]: https://bintray.com/google/exoplayer
-[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
### Locally ###
@@ -106,15 +116,9 @@ compile project(':exoplayer-library-ui)
#### Project branches ####
- * The project has `dev-vX` and `release-vX` branches, where `X` is the major
- version number.
- * Most development work happens on the `dev-vX` branch with the highest major
- version number. Pull requests should normally be made to this branch.
- * Bug fixes may be submitted to older `dev-vX` branches. When doing this, the
- same (or an equivalent) fix should also be submitted to all subsequent
- `dev-vX` branches.
- * A `release-vX` branch holds the most recent stable release for major version
- `X`.
+* Development work happens on the `dev-v2` branch. Pull requests should
+ normally be made to this branch.
+* The `release-v2` branch holds the most recent release.
#### Using Android Studio ####
diff --git a/build.gradle b/build.gradle
index dbc8a41eb0..d5cc64baa1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,6 +14,9 @@
buildscript {
repositories {
jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
diff --git a/constants.gradle b/constants.gradle
index b7cc8b6906..4107faab4c 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -12,16 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
- // Important: ExoPlayer specifies a minSdkVersion of 9 because various
+ // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater.
- minSdkVersion = 9
- compileSdkVersion = 25
- targetSdkVersion = 25
- buildToolsVersion = '25'
+ minSdkVersion = 14
+ compileSdkVersion = 26
+ targetSdkVersion = 26
+ buildToolsVersion = '26'
testSupportLibraryVersion = '0.5'
- supportLibraryVersion = '25.4.0'
+ supportLibraryVersion = '26.0.1'
+ playServicesLibraryVersion = '11.0.2'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
releaseVersion = 'r2.5.1'
diff --git a/core_settings.gradle b/core_settings.gradle
index 20e7b235a2..20a7c87bde 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -28,11 +28,13 @@ include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima'
+include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
+include modulePrefix + 'extension-leanback'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
@@ -45,11 +47,13 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
+project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
+project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
if (gradle.ext.has('exoplayerIncludeCronetExtension')
&& gradle.ext.exoplayerIncludeCronetExtension) {
diff --git a/demo/src/main/res/drawable-xhdpi/ic_banner.png b/demo/src/main/res/drawable-xhdpi/ic_banner.png
deleted file mode 100644
index 520d83cc3b..0000000000
Binary files a/demo/src/main/res/drawable-xhdpi/ic_banner.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.png b/demo/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 6e8b5499de..0000000000
Binary files a/demo/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.png b/demo/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 26fe2f0782..0000000000
Binary files a/demo/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index d3251491ce..0000000000
Binary files a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index b5a12d35f3..0000000000
Binary files a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 9c26192c32..0000000000
Binary files a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/demos/README.md b/demos/README.md
new file mode 100644
index 0000000000..7e62249db1
--- /dev/null
+++ b/demos/README.md
@@ -0,0 +1,4 @@
+# ExoPlayer demos #
+
+This directory contains applications that demonstrate how to use ExoPlayer.
+Browse the individual demos and their READMEs to learn more.
diff --git a/demos/cast/README.md b/demos/cast/README.md
new file mode 100644
index 0000000000..2c68a5277a
--- /dev/null
+++ b/demos/cast/README.md
@@ -0,0 +1,4 @@
+# Cast demo application #
+
+This folder contains a demo application that showcases ExoPlayer integration
+with Google Cast.
diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle
new file mode 100644
index 0000000000..a9fa27ad58
--- /dev/null
+++ b/demos/cast/build.gradle
@@ -0,0 +1,51 @@
+// 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.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion 16
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ debug {
+ jniDebuggable = true
+ }
+ }
+
+ lintOptions {
+ // The demo app does not have translations.
+ disable 'MissingTranslation'
+ }
+
+}
+
+dependencies {
+ compile project(modulePrefix + 'library-core')
+ compile project(modulePrefix + 'library-dash')
+ compile project(modulePrefix + 'library-hls')
+ compile project(modulePrefix + 'library-smoothstreaming')
+ compile project(modulePrefix + 'library-ui')
+ compile project(modulePrefix + 'extension-cast')
+}
diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..eeb28438bd
--- /dev/null
+++ b/demos/cast/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java
new file mode 100644
index 0000000000..f819e54e50
--- /dev/null
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java
@@ -0,0 +1,92 @@
+/*
+ * 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.castdemo;
+
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.gms.cast.MediaInfo;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility methods and constants for the Cast demo application.
+ */
+/* package */ final class CastDemoUtil {
+
+ public static final String MIME_TYPE_DASH = "application/dash+xml";
+ public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl";
+ public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml";
+ public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
+
+ /**
+ * The list of samples available in the cast demo app.
+ */
+ public static final List SAMPLES;
+
+ /**
+ * Represents a media sample.
+ */
+ public static final class Sample {
+
+ /**
+ * The uri from which the media sample is obtained.
+ */
+ public final String uri;
+ /**
+ * A descriptive name for the sample.
+ */
+ public final String name;
+ /**
+ * The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
+ */
+ public final String type;
+
+ /**
+ * @param uri See {@link #uri}.
+ * @param name See {@link #name}.
+ * @param type See {@link #type}.
+ */
+ public Sample(String uri, String name, String type) {
+ this.uri = uri;
+ this.name = name;
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ }
+
+ static {
+ // App samples.
+ ArrayList samples = new ArrayList<>();
+ samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
+ "DASH (clear,MP4,H264)", MIME_TYPE_DASH));
+ samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
+ + "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
+ samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
+ MIME_TYPE_VIDEO_MP4));
+
+
+ SAMPLES = Collections.unmodifiableList(samples);
+
+ }
+
+ private CastDemoUtil() {}
+
+}
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java
new file mode 100644
index 0000000000..e1367858aa
--- /dev/null
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java
@@ -0,0 +1,152 @@
+/*
+ * 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.castdemo;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.ext.cast.CastPlayer;
+import com.google.android.exoplayer2.ui.PlaybackControlView;
+import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.gms.cast.framework.CastButtonFactory;
+
+/**
+ * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
+ */
+public class MainActivity extends AppCompatActivity {
+
+ private SimpleExoPlayerView simpleExoPlayerView;
+ private PlaybackControlView castControlView;
+ private PlayerManager playerManager;
+
+ // Activity lifecycle methods.
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.main_activity);
+
+ simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
+ simpleExoPlayerView.requestFocus();
+
+ castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view);
+
+ ListView sampleList = (ListView) findViewById(R.id.sample_list);
+ sampleList.setAdapter(new SampleListAdapter());
+ sampleList.setOnItemClickListener(new SampleClickListener());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.menu, menu);
+ CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
+ R.id.media_route_menu_item);
+ return true;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (Util.SDK_INT > 23) {
+ setupPlayerManager();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if ((Util.SDK_INT <= 23)) {
+ setupPlayerManager();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (Util.SDK_INT <= 23) {
+ releasePlayerManager();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (Util.SDK_INT > 23) {
+ releasePlayerManager();
+ }
+ }
+
+ // Activity input.
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // If the event was not handled then see if the player view can handle it.
+ return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
+ }
+
+ // Internal methods.
+
+ private void setupPlayerManager() {
+ playerManager = new PlayerManager(simpleExoPlayerView, castControlView,
+ getApplicationContext());
+ }
+
+ private void releasePlayerManager() {
+ playerManager.release();
+ playerManager = null;
+ }
+
+ // User controls.
+
+ private final class SampleListAdapter extends ArrayAdapter {
+
+ public SampleListAdapter() {
+ super(getApplicationContext(), android.R.layout.simple_list_item_1, CastDemoUtil.SAMPLES);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ view.setBackgroundColor(Color.WHITE);
+ return view;
+ }
+
+ }
+
+ private class SampleClickListener implements AdapterView.OnItemClickListener {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (parent.getSelectedItemPosition() != position) {
+ CastDemoUtil.Sample currentSample = CastDemoUtil.SAMPLES.get(position);
+ playerManager.setCurrentSample(currentSample, 0, true);
+ }
+ }
+
+ }
+
+}
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
new file mode 100644
index 0000000000..741df7eff1
--- /dev/null
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
@@ -0,0 +1,196 @@
+/*
+ * 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.castdemo;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.KeyEvent;
+import android.view.View;
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.ext.cast.CastPlayer;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.ui.PlaybackControlView;
+import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.gms.cast.framework.CastContext;
+
+/**
+ * Manages players for the ExoPlayer/Cast integration app.
+ */
+/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener {
+
+ private static final int PLAYBACK_REMOTE = 1;
+ private static final int PLAYBACK_LOCAL = 2;
+
+ private static final String USER_AGENT = "ExoCastDemoPlayer";
+ private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
+ private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
+ new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
+
+ private final SimpleExoPlayerView exoPlayerView;
+ private final PlaybackControlView castControlView;
+ private final CastContext castContext;
+ private final SimpleExoPlayer exoPlayer;
+ private final CastPlayer castPlayer;
+
+ private int playbackLocation;
+ private CastDemoUtil.Sample currentSample;
+
+ /**
+ * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback.
+ * @param castControlView The {@link PlaybackControlView} to control remote playback.
+ * @param context A {@link Context}.
+ */
+ public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView,
+ Context context) {
+ this.exoPlayerView = exoPlayerView;
+ this.castControlView = castControlView;
+ castContext = CastContext.getSharedInstance(context);
+
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
+ exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
+ exoPlayerView.setPlayer(exoPlayer);
+
+ castPlayer = new CastPlayer(castContext);
+ castPlayer.setSessionAvailabilityListener(this);
+ castControlView.setPlayer(castPlayer);
+
+ setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL);
+ }
+
+ /**
+ * Starts playback of the given sample at the given position.
+ *
+ * @param currentSample The {@link CastDemoUtil} to play.
+ * @param positionMs The position at which playback should start.
+ * @param playWhenReady Whether the player should proceed when ready to do so.
+ */
+ public void setCurrentSample(CastDemoUtil.Sample currentSample, long positionMs,
+ boolean playWhenReady) {
+ this.currentSample = currentSample;
+ if (playbackLocation == PLAYBACK_REMOTE) {
+ castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs,
+ playWhenReady);
+ } else /* playbackLocation == PLAYBACK_LOCAL */ {
+ exoPlayer.setPlayWhenReady(playWhenReady);
+ exoPlayer.seekTo(positionMs);
+ exoPlayer.prepare(buildMediaSource(currentSample), true, true);
+ }
+ }
+
+ /**
+ * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current
+ * playback location.
+ *
+ * @param event The {@link KeyEvent}.
+ * @return Whether the event was handled by the target view.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (playbackLocation == PLAYBACK_REMOTE) {
+ return castControlView.dispatchKeyEvent(event);
+ } else /* playbackLocation == PLAYBACK_REMOTE */ {
+ return exoPlayerView.dispatchKeyEvent(event);
+ }
+ }
+
+ /**
+ * Releases the manager and the players that it holds.
+ */
+ public void release() {
+ castPlayer.setSessionAvailabilityListener(null);
+ castPlayer.release();
+ exoPlayerView.setPlayer(null);
+ exoPlayer.release();
+ }
+
+ // CastPlayer.SessionAvailabilityListener implementation.
+
+ @Override
+ public void onCastSessionAvailable() {
+ setPlaybackLocation(PLAYBACK_REMOTE);
+ }
+
+ @Override
+ public void onCastSessionUnavailable() {
+ setPlaybackLocation(PLAYBACK_LOCAL);
+ }
+
+ // Internal methods.
+
+ private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) {
+ Uri uri = Uri.parse(sample.uri);
+ switch (sample.type) {
+ case CastDemoUtil.MIME_TYPE_SS:
+ return new SsMediaSource(uri, DATA_SOURCE_FACTORY,
+ new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
+ case CastDemoUtil.MIME_TYPE_DASH:
+ return new DashMediaSource(uri, DATA_SOURCE_FACTORY,
+ new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
+ case CastDemoUtil.MIME_TYPE_HLS:
+ return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null);
+ case CastDemoUtil.MIME_TYPE_VIDEO_MP4:
+ return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(),
+ null, null);
+ default: {
+ throw new IllegalStateException("Unsupported type: " + sample.type);
+ }
+ }
+ }
+
+ private void setPlaybackLocation(int playbackLocation) {
+ if (this.playbackLocation == playbackLocation) {
+ return;
+ }
+
+ // View management.
+ if (playbackLocation == PLAYBACK_LOCAL) {
+ exoPlayerView.setVisibility(View.VISIBLE);
+ castControlView.hide();
+ } else {
+ exoPlayerView.setVisibility(View.GONE);
+ castControlView.show();
+ }
+
+ long playbackPositionMs = 0;
+ boolean playWhenReady = true;
+ if (exoPlayer != null) {
+ playbackPositionMs = exoPlayer.getCurrentPosition();
+ playWhenReady = exoPlayer.getPlayWhenReady();
+ } else if (this.playbackLocation == PLAYBACK_REMOTE) {
+ playbackPositionMs = castPlayer.getCurrentPosition();
+ playWhenReady = castPlayer.getPlayWhenReady();
+ }
+
+ this.playbackLocation = playbackLocation;
+ if (currentSample != null) {
+ setCurrentSample(currentSample, playbackPositionMs, playWhenReady);
+ }
+ }
+
+}
diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000000..7e39320e3b
--- /dev/null
+++ b/demos/cast/src/main/res/layout/main_activity.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/demos/cast/src/main/res/menu/menu.xml b/demos/cast/src/main/res/menu/menu.xml
new file mode 100644
index 0000000000..075ad34ec4
--- /dev/null
+++ b/demos/cast/src/main/res/menu/menu.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..52e8dc93d9
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..b55576eff3
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..ca84d6a60e
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..27ab9b1054
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..d1eb9b78cf
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..503892da27
--- /dev/null
+++ b/demos/cast/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ ExoCast Demo
+
+ ExoCast
+
+ DRM scheme not supported by this device.
+
+
diff --git a/demos/cast/src/main/res/values/styles.xml b/demos/cast/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..1484a68a68
--- /dev/null
+++ b/demos/cast/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/demo/README.md b/demos/main/README.md
similarity index 58%
rename from demo/README.md
rename to demos/main/README.md
index ca37392623..bdb04e5ba8 100644
--- a/demo/README.md
+++ b/demos/main/README.md
@@ -1,5 +1,5 @@
-# Demo application #
+# ExoPlayer main demo #
-This folder contains a demo application that uses ExoPlayer to play a number
+This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
of test streams. It can be used as a starting point or reference project when
developing other applications that make use of the ExoPlayer library.
diff --git a/demo/build.gradle b/demos/main/build.gradle
similarity index 90%
rename from demo/build.gradle
rename to demos/main/build.gradle
index 7eea25478f..099741d167 100644
--- a/demo/build.gradle
+++ b/demos/main/build.gradle
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-apply from: '../constants.gradle'
+apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
@@ -39,9 +39,15 @@ android {
disable 'MissingTranslation'
}
+ flavorDimensions "extensions"
+
productFlavors {
- noExtensions
- withExtensions
+ noExtensions {
+ dimension "extensions"
+ }
+ withExtensions {
+ dimension "extensions"
+ }
}
}
diff --git a/demo/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml
similarity index 99%
rename from demo/src/main/AndroidManifest.xml
rename to demos/main/src/main/AndroidManifest.xml
index 1f66822dc7..4f90cef623 100644
--- a/demo/src/main/AndroidManifest.xml
+++ b/demos/main/src/main/AndroidManifest.xml
@@ -23,7 +23,7 @@
-
+
+
+
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
new file mode 100644
index 0000000000..50ae7ea5ba
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
@@ -0,0 +1,660 @@
+/*
+ * 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.ext.cast;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.gms.cast.CastStatusCodes;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.MediaTrack;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.cast.framework.SessionManager;
+import com.google.android.gms.cast.framework.SessionManagerListener;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
+import com.google.android.gms.common.api.CommonStatusCodes;
+import com.google.android.gms.common.api.ResultCallback;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * {@link Player} implementation that communicates with a Cast receiver app.
+ *
+ *
Calls to the methods in this class depend on the availability of an underlying cast session.
+ * If no session is available, method calls have no effect. To keep track of the underyling session,
+ * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
+ * implemented and attached to the player.
+ *
+ *
Methods should be called on the application's main thread.
+ *
+ *
Known issues:
+ *
+ *
Part of the Cast API is not exposed through this interface. For instance, volume settings
+ * and track selection.
+ *
Repeat mode is not working. See [internal: b/64137174].
+ *
+ */
+public final class CastPlayer implements Player {
+
+ /**
+ * Listener of changes in the cast session availability.
+ */
+ public interface SessionAvailabilityListener {
+
+ /**
+ * Called when a cast session becomes available to the player.
+ */
+ void onCastSessionAvailable();
+
+ /**
+ * Called when the cast session becomes unavailable.
+ */
+ void onCastSessionUnavailable();
+
+ }
+
+ private static final String TAG = "CastPlayer";
+
+ private static final int RENDERER_COUNT = 3;
+ private static final int RENDERER_INDEX_VIDEO = 0;
+ private static final int RENDERER_INDEX_AUDIO = 1;
+ private static final int RENDERER_INDEX_TEXT = 2;
+ private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
+ private static final TrackGroupArray EMPTY_TRACK_GROUP_ARRAY = new TrackGroupArray();
+ private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
+ new TrackSelectionArray(null, null, null);
+ private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
+
+ private final CastContext castContext;
+ private final Timeline.Window window;
+
+ // Result callbacks.
+ private final StatusListener statusListener;
+ private final RepeatModeResultCallback repeatModeResultCallback;
+ private final SeekResultCallback seekResultCallback;
+
+ // Listeners.
+ private final CopyOnWriteArraySet listeners;
+ private SessionAvailabilityListener sessionAvailabilityListener;
+
+ // Internal state.
+ private RemoteMediaClient remoteMediaClient;
+ private Timeline currentTimeline;
+ private TrackGroupArray currentTrackGroups;
+ private TrackSelectionArray currentTrackSelection;
+ private long lastReportedPositionMs;
+ private long pendingSeekPositionMs;
+
+ /**
+ * @param castContext The context from which the cast session is obtained.
+ */
+ public CastPlayer(CastContext castContext) {
+ this.castContext = castContext;
+ window = new Timeline.Window();
+ statusListener = new StatusListener();
+ repeatModeResultCallback = new RepeatModeResultCallback();
+ seekResultCallback = new SeekResultCallback();
+ listeners = new CopyOnWriteArraySet<>();
+ SessionManager sessionManager = castContext.getSessionManager();
+ sessionManager.addSessionManagerListener(statusListener, CastSession.class);
+ CastSession session = sessionManager.getCurrentCastSession();
+ remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
+ pendingSeekPositionMs = C.TIME_UNSET;
+ updateInternalState();
+ }
+
+ /**
+ * Loads media into the receiver app.
+ *
+ * @param title The title of the media sample.
+ * @param url The url from which the media is obtained.
+ * @param contentMimeType The mime type of the content to play.
+ * @param positionMs The position at which the playback should start in milliseconds.
+ * @param playWhenReady Whether the player should start playback as soon as it is ready to do so.
+ */
+ public void load(String title, String url, String contentMimeType, long positionMs,
+ boolean playWhenReady) {
+ lastReportedPositionMs = 0;
+ if (remoteMediaClient != null) {
+ MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
+ movieMetadata.putString(MediaMetadata.KEY_TITLE, title);
+ MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setContentType(contentMimeType).setMetadata(movieMetadata).build();
+ remoteMediaClient.load(mediaInfo, playWhenReady, positionMs);
+ }
+ }
+
+ /**
+ * Returns whether a cast session is available for playback.
+ */
+ public boolean isCastSessionAvailable() {
+ return remoteMediaClient != null;
+ }
+
+ /**
+ * Sets a listener for updates on the cast session availability.
+ *
+ * @param listener The {@link SessionAvailabilityListener}.
+ */
+ public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
+ sessionAvailabilityListener = listener;
+ }
+
+ // Player implementation.
+
+ @Override
+ public void addListener(EventListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(EventListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public int getPlaybackState() {
+ if (remoteMediaClient == null) {
+ return STATE_IDLE;
+ }
+ int receiverAppStatus = remoteMediaClient.getPlayerState();
+ switch (receiverAppStatus) {
+ case MediaStatus.PLAYER_STATE_BUFFERING:
+ return STATE_BUFFERING;
+ case MediaStatus.PLAYER_STATE_PLAYING:
+ case MediaStatus.PLAYER_STATE_PAUSED:
+ return STATE_READY;
+ case MediaStatus.PLAYER_STATE_IDLE:
+ case MediaStatus.PLAYER_STATE_UNKNOWN:
+ default:
+ return STATE_IDLE;
+ }
+ }
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ if (remoteMediaClient == null) {
+ return;
+ }
+ if (playWhenReady) {
+ remoteMediaClient.play();
+ } else {
+ remoteMediaClient.pause();
+ }
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ return remoteMediaClient != null && !remoteMediaClient.isPaused();
+ }
+
+ @Override
+ public void seekToDefaultPosition() {
+ seekTo(0);
+ }
+
+ @Override
+ public void seekToDefaultPosition(int windowIndex) {
+ seekTo(windowIndex, 0);
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ seekTo(0, positionMs);
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ if (remoteMediaClient != null) {
+ remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
+ pendingSeekPositionMs = positionMs;
+ for (EventListener listener : listeners) {
+ listener.onPositionDiscontinuity();
+ }
+ }
+ }
+
+ @Override
+ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ // Unsupported by the RemoteMediaClient API. Do nothing.
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return PlaybackParameters.DEFAULT;
+ }
+
+ @Override
+ public void stop() {
+ if (remoteMediaClient != null) {
+ remoteMediaClient.stop();
+ }
+ }
+
+ @Override
+ public void release() {
+ castContext.getSessionManager().removeSessionManagerListener(statusListener, CastSession.class);
+ }
+
+ @Override
+ public int getRendererCount() {
+ // We assume there are three renderers: video, audio, and text.
+ return RENDERER_COUNT;
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ switch (index) {
+ case RENDERER_INDEX_VIDEO:
+ return C.TRACK_TYPE_VIDEO;
+ case RENDERER_INDEX_AUDIO:
+ return C.TRACK_TYPE_AUDIO;
+ case RENDERER_INDEX_TEXT:
+ return C.TRACK_TYPE_TEXT;
+ default:
+ throw new IndexOutOfBoundsException();
+ }
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ if (remoteMediaClient != null) {
+ int castRepeatMode;
+ switch (repeatMode) {
+ case REPEAT_MODE_ONE:
+ castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
+ break;
+ case REPEAT_MODE_ALL:
+ castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL;
+ break;
+ case REPEAT_MODE_OFF:
+ castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ remoteMediaClient.queueSetRepeatMode(castRepeatMode, null)
+ .setResultCallback(repeatModeResultCallback);
+ }
+ }
+
+ @Override
+ @RepeatMode public int getRepeatMode() {
+ if (remoteMediaClient == null) {
+ return REPEAT_MODE_OFF;
+ }
+ MediaStatus mediaStatus = getMediaStatus();
+ if (mediaStatus == null) {
+ // No media session active, yet.
+ return REPEAT_MODE_OFF;
+ }
+ int castRepeatMode = mediaStatus.getQueueRepeatMode();
+ switch (castRepeatMode) {
+ case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
+ return REPEAT_MODE_ONE;
+ case MediaStatus.REPEAT_MODE_REPEAT_ALL:
+ case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
+ return REPEAT_MODE_ALL;
+ case MediaStatus.REPEAT_MODE_REPEAT_OFF:
+ return REPEAT_MODE_OFF;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ // TODO: Support shuffle mode.
+ }
+
+ @Override
+ public boolean getShuffleModeEnabled() {
+ // TODO: Support shuffle mode.
+ return false;
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return currentTrackSelection;
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ return currentTrackGroups;
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ return currentTimeline;
+ }
+
+ @Override
+ @Nullable public Object getCurrentManifest() {
+ return null;
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ return 0;
+ }
+
+ @Override
+ public long getDuration() {
+ return currentTimeline.isEmpty() ? C.TIME_UNSET
+ : currentTimeline.getWindow(0, window).getDurationMs();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return remoteMediaClient == null ? lastReportedPositionMs
+ : pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs
+ : remoteMediaClient.getApproximateStreamPosition();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return getCurrentPosition();
+ }
+
+ @Override
+ public int getBufferedPercentage() {
+ long position = getBufferedPosition();
+ long duration = getDuration();
+ return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
+ : duration == 0 ? 100
+ : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
+ }
+
+ @Override
+ public boolean isCurrentWindowDynamic() {
+ return !currentTimeline.isEmpty()
+ && currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
+ }
+
+ @Override
+ public boolean isCurrentWindowSeekable() {
+ return !currentTimeline.isEmpty()
+ && currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
+ }
+
+ @Override
+ public boolean isPlayingAd() {
+ return false;
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return false;
+ }
+
+ @Override
+ public long getContentPosition() {
+ return getCurrentPosition();
+ }
+
+ // Internal methods.
+
+ private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
+ if (this.remoteMediaClient == remoteMediaClient) {
+ // Do nothing.
+ return;
+ }
+ if (this.remoteMediaClient != null) {
+ this.remoteMediaClient.removeListener(statusListener);
+ this.remoteMediaClient.removeProgressListener(statusListener);
+ }
+ this.remoteMediaClient = remoteMediaClient;
+ if (remoteMediaClient != null) {
+ if (sessionAvailabilityListener != null) {
+ sessionAvailabilityListener.onCastSessionAvailable();
+ }
+ remoteMediaClient.addListener(statusListener);
+ remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
+ } else {
+ if (sessionAvailabilityListener != null) {
+ sessionAvailabilityListener.onCastSessionUnavailable();
+ }
+ }
+ }
+
+ private @Nullable MediaStatus getMediaStatus() {
+ return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
+ }
+
+ private @Nullable MediaInfo getMediaInfo() {
+ return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null;
+ }
+
+ private void updateInternalState() {
+ currentTimeline = Timeline.EMPTY;
+ currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY;
+ currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
+ MediaInfo mediaInfo = getMediaInfo();
+ if (mediaInfo == null) {
+ return;
+ }
+ long streamDurationMs = mediaInfo.getStreamDuration();
+ boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION;
+ currentTimeline = new SinglePeriodTimeline(
+ isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable);
+
+ List tracks = mediaInfo.getMediaTracks();
+ if (tracks == null) {
+ return;
+ }
+
+ MediaStatus mediaStatus = getMediaStatus();
+ long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null;
+ if (activeTrackIds == null) {
+ activeTrackIds = EMPTY_TRACK_ID_ARRAY;
+ }
+
+ TrackGroup[] trackGroups = new TrackGroup[tracks.size()];
+ TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
+ for (int i = 0; i < tracks.size(); i++) {
+ MediaTrack mediaTrack = tracks.get(i);
+ trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
+
+ long id = mediaTrack.getId();
+ int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
+ int rendererIndex = getRendererIndexForTrackType(trackType);
+ if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
+ && trackSelections[rendererIndex] == null) {
+ trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
+ }
+ }
+ currentTrackSelection = new TrackSelectionArray(trackSelections);
+ currentTrackGroups = new TrackGroupArray(trackGroups);
+ }
+
+ private static boolean isTrackActive(long id, long[] activeTrackIds) {
+ for (long activeTrackId : activeTrackIds) {
+ if (activeTrackId == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int getRendererIndexForTrackType(int trackType) {
+ return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO
+ : trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO
+ : trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT
+ : C.INDEX_UNSET;
+ }
+
+ private final class StatusListener implements RemoteMediaClient.Listener,
+ SessionManagerListener, RemoteMediaClient.ProgressListener {
+
+ // RemoteMediaClient.ProgressListener implementation.
+
+ @Override
+ public void onProgressUpdated(long progressMs, long unusedDurationMs) {
+ lastReportedPositionMs = progressMs;
+ }
+
+ // RemoteMediaClient.Listener implementation.
+
+ @Override
+ public void onStatusUpdated() {
+ boolean playWhenReady = getPlayWhenReady();
+ int playbackState = getPlaybackState();
+ for (EventListener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ }
+
+ @Override
+ public void onMetadataUpdated() {
+ updateInternalState();
+ for (EventListener listener : listeners) {
+ listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
+ listener.onTimelineChanged(currentTimeline, null);
+ }
+ }
+
+ @Override
+ public void onQueueStatusUpdated() {}
+
+ @Override
+ public void onPreloadStatusUpdated() {}
+
+ @Override
+ public void onSendingRemoteMediaRequest() {}
+
+ @Override
+ public void onAdBreakStatusUpdated() {}
+
+
+ // SessionManagerListener implementation.
+
+ @Override
+ public void onSessionStarted(CastSession castSession, String s) {
+ setRemoteMediaClient(castSession.getRemoteMediaClient());
+ }
+
+ @Override
+ public void onSessionResumed(CastSession castSession, boolean b) {
+ setRemoteMediaClient(castSession.getRemoteMediaClient());
+ }
+
+ @Override
+ public void onSessionEnded(CastSession castSession, int i) {
+ setRemoteMediaClient(null);
+ }
+
+ @Override
+ public void onSessionSuspended(CastSession castSession, int i) {
+ setRemoteMediaClient(null);
+ }
+
+ @Override
+ public void onSessionResumeFailed(CastSession castSession, int statusCode) {
+ Log.e(TAG, "Session resume failed. Error code " + statusCode + ": "
+ + CastUtils.getLogString(statusCode));
+ }
+
+ @Override
+ public void onSessionStarting(CastSession castSession) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSessionStartFailed(CastSession castSession, int statusCode) {
+ Log.e(TAG, "Session start failed. Error code " + statusCode + ": "
+ + CastUtils.getLogString(statusCode));
+ }
+
+ @Override
+ public void onSessionEnding(CastSession castSession) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSessionResuming(CastSession castSession, String s) {
+ // Do nothing.
+ }
+
+ }
+
+ // Result callbacks hooks.
+
+ private final class RepeatModeResultCallback implements ResultCallback {
+
+ @Override
+ public void onResult(MediaChannelResult result) {
+ int statusCode = result.getStatus().getStatusCode();
+ if (statusCode == CommonStatusCodes.SUCCESS) {
+ int repeatMode = getRepeatMode();
+ for (EventListener listener : listeners) {
+ listener.onRepeatModeChanged(repeatMode);
+ }
+ } else {
+ Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": "
+ + CastUtils.getLogString(statusCode));
+ }
+ }
+
+ }
+
+ private final class SeekResultCallback implements ResultCallback {
+
+ @Override
+ public void onResult(MediaChannelResult result) {
+ int statusCode = result.getStatus().getStatusCode();
+ if (statusCode == CommonStatusCodes.SUCCESS) {
+ pendingSeekPositionMs = C.TIME_UNSET;
+ } else if (statusCode == CastStatusCodes.REPLACED) {
+ // A seek was executed before this one completed. Do nothing.
+ } else {
+ Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
+ + CastUtils.getLogString(statusCode));
+ }
+ }
+
+ }
+
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
new file mode 100644
index 0000000000..de60437444
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ext.cast;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.gms.cast.CastStatusCodes;
+import com.google.android.gms.cast.MediaTrack;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility methods for ExoPlayer/Cast integration.
+ */
+/* package */ final class CastUtils {
+
+ private static final Map CAST_STATUS_CODE_TO_STRING;
+
+ /**
+ * Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
+ * {@link CastStatusCodes}.
+ *
+ * @param statusCode A Cast API status code.
+ * @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
+ * {@link CastStatusCodes}.
+ */
+ public static String getLogString(int statusCode) {
+ String description = CAST_STATUS_CODE_TO_STRING.get(statusCode);
+ return description != null ? description : "Unknown.";
+ }
+
+ /**
+ * Creates a {@link Format} instance containing all information contained in the given
+ * {@link MediaTrack} object.
+ *
+ * @param mediaTrack The {@link MediaTrack}.
+ * @return The equivalent {@link Format}.
+ */
+ public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
+ return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
+ null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
+ }
+
+ static {
+ HashMap statusCodeToString = new HashMap<>();
+ statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_FOUND,
+ "A requested application could not be found.");
+ statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_RUNNING,
+ "A requested application is not currently running.");
+ statusCodeToString.put(CastStatusCodes.AUTHENTICATION_FAILED, "Authentication failure.");
+ statusCodeToString.put(CastStatusCodes.CANCELED, "An in-progress request has been "
+ + "canceled, most likely because another action has preempted it.");
+ statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_CREATION_FAILED,
+ "The Cast Remote Display service could not be created.");
+ statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_DISCONNECTED,
+ "The Cast Remote Display service was disconnected.");
+ statusCodeToString.put(CastStatusCodes.FAILED, "The in-progress request failed.");
+ statusCodeToString.put(CastStatusCodes.INTERNAL_ERROR, "An internal error has occurred.");
+ statusCodeToString.put(CastStatusCodes.INTERRUPTED,
+ "A blocking call was interrupted while waiting and did not run to completion.");
+ statusCodeToString.put(CastStatusCodes.INVALID_REQUEST, "An invalid request was made.");
+ statusCodeToString.put(CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL, "A message could "
+ + "not be sent because there is not enough room in the send buffer at this time.");
+ statusCodeToString.put(CastStatusCodes.MESSAGE_TOO_LARGE,
+ "A message could not be sent because it is too large.");
+ statusCodeToString.put(CastStatusCodes.NETWORK_ERROR, "Network I/O error.");
+ statusCodeToString.put(CastStatusCodes.NOT_ALLOWED,
+ "The request was disallowed and could not be completed.");
+ statusCodeToString.put(CastStatusCodes.REPLACED,
+ "The request's progress is no longer being tracked because another request of the same type"
+ + " has been made before the first request completed.");
+ statusCodeToString.put(CastStatusCodes.SUCCESS, "Success.");
+ statusCodeToString.put(CastStatusCodes.TIMEOUT, "An operation has timed out.");
+ statusCodeToString.put(CastStatusCodes.UNKNOWN_ERROR,
+ "An unknown, unexpected error has occurred.");
+ CAST_STATUS_CODE_TO_STRING = Collections.unmodifiableMap(statusCodeToString);
+ }
+
+ private CastUtils() {}
+
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
new file mode 100644
index 0000000000..06f0bec971
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.ext.cast;
+
+import android.content.Context;
+import com.google.android.gms.cast.CastMediaControlIntent;
+import com.google.android.gms.cast.framework.CastOptions;
+import com.google.android.gms.cast.framework.OptionsProvider;
+import com.google.android.gms.cast.framework.SessionProvider;
+import java.util.List;
+
+/**
+ * A convenience {@link OptionsProvider} to target the default cast receiver app.
+ */
+public final class DefaultCastOptionsProvider implements OptionsProvider {
+
+ @Override
+ public CastOptions getCastOptions(Context context) {
+ return new CastOptions.Builder()
+ .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
+ .setStopReceiverApplicationWhenEndingSession(true).build();
+ }
+
+ @Override
+ public List getAdditionalSessionProviders(Context context) {
+ return null;
+ }
+
+}
diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md
index 2287c4c19b..66da774978 100644
--- a/extensions/cronet/README.md
+++ b/extensions/cronet/README.md
@@ -1,10 +1,8 @@
# ExoPlayer Cronet extension #
-## Description ##
-
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html
+[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Build instructions ##
@@ -22,12 +20,9 @@ and enable the extension:
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension
-* In your `settings.gradle` file, add the following line before the line that
- applies `core_settings.gradle`:
-
-```gradle
-gradle.ext.exoplayerIncludeCronetExtension = true;
-```
+* In your `settings.gradle` file, add
+ `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
+ applies `core_settings.gradle`.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
@@ -56,3 +51,10 @@ new DefaultDataSourceFactory(
new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */);
```
respectively.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
+ belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml
index 1f371a1864..7f14a28e83 100644
--- a/extensions/cronet/src/androidTest/AndroidManifest.xml
+++ b/extensions/cronet/src/androidTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.ext.cronet">
-
+
-
+
+
+
+
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
new file mode 100644
index 0000000000..8a207bea8f
--- /dev/null
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -0,0 +1,295 @@
+/*
+ * 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.ext.leanback;
+
+import android.content.Context;
+import android.os.Handler;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.media.PlayerAdapter;
+import android.support.v17.leanback.media.SurfaceHolderGlueHost;
+import android.util.Pair;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.ErrorMessageProvider;
+
+/**
+ * Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}.
+ */
+public final class LeanbackPlayerAdapter extends PlayerAdapter {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
+ }
+
+ private final Context context;
+ private final SimpleExoPlayer player;
+ private final Handler handler;
+ private final ComponentListener componentListener;
+ private final Runnable updatePlayerRunnable;
+
+ private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ private SurfaceHolderGlueHost surfaceHolderGlueHost;
+ private boolean initialized;
+ private boolean hasSurface;
+ private boolean isBuffering;
+
+ /**
+ * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the
+ * {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when
+ * it's no longer required.
+ *
+ * @param context The current context (activity).
+ * @param player Instance of your exoplayer that needs to be configured.
+ * @param updatePeriodMs The delay between player control updates, in milliseconds.
+ */
+ public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) {
+ this.context = context;
+ this.player = player;
+ handler = new Handler();
+ componentListener = new ComponentListener();
+ updatePlayerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ Callback callback = getCallback();
+ callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
+ callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
+ handler.postDelayed(this, updatePeriodMs);
+ }
+ };
+ }
+
+ @Override
+ public void onAttachedToHost(PlaybackGlueHost host) {
+ if (host instanceof SurfaceHolderGlueHost) {
+ surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
+ surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);
+ }
+ notifyListeners();
+ player.addListener(componentListener);
+ player.addVideoListener(componentListener);
+ }
+
+ private void notifyListeners() {
+ boolean oldIsPrepared = isPrepared();
+ int playbackState = player.getPlaybackState();
+ boolean isInitialized = playbackState != Player.STATE_IDLE;
+ isBuffering = playbackState == Player.STATE_BUFFERING;
+ boolean hasEnded = playbackState == Player.STATE_ENDED;
+
+ initialized = isInitialized;
+ Callback callback = getCallback();
+ if (oldIsPrepared != isPrepared()) {
+ callback.onPreparedStateChanged(this);
+ }
+ callback.onPlayStateChanged(this);
+ callback.onBufferingStateChanged(this, isBuffering || !initialized);
+ if (hasEnded) {
+ callback.onPlayCompleted(this);
+ }
+ }
+
+ /**
+ * Sets the optional {@link ErrorMessageProvider}.
+ *
+ * @param errorMessageProvider The {@link ErrorMessageProvider}.
+ */
+ public void setErrorMessageProvider(
+ ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
+ this.errorMessageProvider = errorMessageProvider;
+ }
+
+ @Override
+ public void onDetachedFromHost() {
+ player.removeListener(componentListener);
+ player.removeVideoListener(componentListener);
+ if (surfaceHolderGlueHost != null) {
+ surfaceHolderGlueHost.setSurfaceHolderCallback(null);
+ surfaceHolderGlueHost = null;
+ }
+ initialized = false;
+ hasSurface = false;
+ Callback callback = getCallback();
+ callback.onBufferingStateChanged(this, false);
+ callback.onPlayStateChanged(this);
+ callback.onPreparedStateChanged(this);
+ }
+
+ @Override
+ public void setProgressUpdatingEnabled(final boolean enabled) {
+ handler.removeCallbacks(updatePlayerRunnable);
+ if (enabled) {
+ handler.post(updatePlayerRunnable);
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return initialized && player.getPlayWhenReady();
+ }
+
+ @Override
+ public long getDuration() {
+ long durationMs = player.getDuration();
+ return durationMs != C.TIME_UNSET ? durationMs : -1;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return initialized ? player.getCurrentPosition() : -1;
+ }
+
+ @Override
+ public void play() {
+ if (player.getPlaybackState() == Player.STATE_ENDED) {
+ player.seekToDefaultPosition();
+ }
+ player.setPlayWhenReady(true);
+ getCallback().onPlayStateChanged(this);
+ }
+
+ @Override
+ public void pause() {
+ player.setPlayWhenReady(false);
+ getCallback().onPlayStateChanged(this);
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ player.seekTo(positionMs);
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return player.getBufferedPosition();
+ }
+
+ @Override
+ public boolean isPrepared() {
+ return initialized && (surfaceHolderGlueHost == null || hasSurface);
+ }
+
+ private void setVideoSurface(Surface surface) {
+ hasSurface = surface != null;
+ player.setVideoSurface(surface);
+ getCallback().onPreparedStateChanged(this);
+ }
+
+ private final class ComponentListener implements Player.EventListener,
+ SimpleExoPlayer.VideoListener, SurfaceHolder.Callback {
+
+ // SurfaceHolder.Callback implementation.
+
+ @Override
+ public void surfaceCreated(SurfaceHolder surfaceHolder) {
+ setVideoSurface(surfaceHolder.getSurface());
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
+ setVideoSurface(null);
+ }
+
+ // Player.EventListener implementation.
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ notifyListeners();
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException exception) {
+ Callback callback = getCallback();
+ if (errorMessageProvider != null) {
+ Pair errorMessage = errorMessageProvider.getErrorMessage(exception);
+ callback.onError(LeanbackPlayerAdapter.this, errorMessage.first, errorMessage.second);
+ } else {
+ callback.onError(LeanbackPlayerAdapter.this, exception.type, context.getString(
+ R.string.lb_media_player_error, exception.type, exception.rendererIndex));
+ }
+ }
+
+ @Override
+ public void onLoadingChanged(boolean isLoading) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, Object manifest) {
+ Callback callback = getCallback();
+ callback.onDurationChanged(LeanbackPlayerAdapter.this);
+ callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
+ callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ Callback callback = getCallback();
+ callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
+ callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters params) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ // Do nothing.
+ }
+
+ // SimpleExoplayerView.Callback implementation.
+
+ @Override
+ public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+ getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);
+ }
+
+ @Override
+ public void onRenderedFirstFrame() {
+ // Do nothing.
+ }
+
+ }
+
+}
+
diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md
index 3acf8e4c79..3278e8dba5 100644
--- a/extensions/mediasession/README.md
+++ b/extensions/mediasession/README.md
@@ -1,11 +1,9 @@
# ExoPlayer MediaSession extension #
-## Description ##
-
-The MediaSession extension mediates between an ExoPlayer instance and a
-[MediaSession][]. It automatically retrieves and implements playback actions
-and syncs the player state with the state of the media session. The behaviour
-can be extended to support other playback and custom actions.
+The MediaSession extension mediates between a Player (or ExoPlayer) instance
+and a [MediaSession][]. It automatically retrieves and implements playback
+actions and syncs the player state with the state of the media session. The
+behaviour can be extended to support other playback and custom actions.
[MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html
@@ -25,3 +23,10 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+
+## Links ##
+
+* [Javadoc][]: Classes matching
+ `com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 0e839b8083..4dc1100c1e 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -38,6 +38,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.ErrorMessageProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -47,7 +48,7 @@ import java.util.Map;
* Connects a {@link MediaSessionCompat} to a {@link Player}.
*
* The connector listens for actions sent by the media session's controller and implements these
- * actions by calling appropriate ExoPlayer methods. The playback state of the media session is
+ * actions by calling appropriate player methods. The playback state of the media session is
* automatically synced with the player. The connector can also be optionally extended by providing
* various collaborators:
*
@@ -73,6 +74,10 @@ public final class MediaSessionConnector {
}
public static final String EXTRAS_PITCH = "EXO_PITCH";
+ private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
+ private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
+ | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
/**
* Interface to which playback preparation actions are delegated.
@@ -293,17 +298,6 @@ public final class MediaSessionConnector {
PlaybackStateCompat.CustomAction getCustomAction();
}
- /**
- * Converts an exception into an error code and a user readable error message.
- */
- public interface ErrorMessageProvider {
- /**
- * Returns a pair consisting of an error code and a user readable error message for a given
- * exception.
- */
- Pair getErrorMessage(ExoPlaybackException playbackException);
- }
-
/**
* The wrapped {@link MediaSessionCompat}.
*/
@@ -318,9 +312,8 @@ public final class MediaSessionConnector {
private Player player;
private CustomActionProvider[] customActionProviders;
- private int currentWindowIndex;
private Map customActionMap;
- private ErrorMessageProvider errorMessageProvider;
+ private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator;
private QueueEditor queueEditor;
@@ -369,8 +362,7 @@ public final class MediaSessionConnector {
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
: Looper.getMainLooper());
this.doMaintainMetadata = doMaintainMetadata;
- mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
- | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
mediaController = mediaSession.getController();
mediaSessionCallback = new MediaSessionCallback();
exoPlayerEventListener = new ExoPlayerEventListener();
@@ -411,7 +403,8 @@ public final class MediaSessionConnector {
*
* @param errorMessageProvider The error message provider.
*/
- public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) {
+ public void setErrorMessageProvider(
+ ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
}
@@ -433,6 +426,8 @@ public final class MediaSessionConnector {
*/
public void setQueueEditor(QueueEditor queueEditor) {
this.queueEditor = queueEditor;
+ mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS
+ : EDITOR_MEDIA_SESSION_FLAGS);
}
private void updateMediaSessionPlaybackState() {
@@ -583,11 +578,20 @@ public final class MediaSessionConnector {
private class ExoPlayerEventListener implements Player.EventListener {
+ private int currentWindowIndex;
+ private int currentWindowCount;
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (queueNavigator != null) {
queueNavigator.onTimelineChanged(player);
}
+ int windowCount = player.getCurrentTimeline().getWindowCount();
+ if (currentWindowCount != windowCount) {
+ // active queue item and queue navigation actions may need to be updated
+ updateMediaSessionPlaybackState();
+ }
+ currentWindowCount = windowCount;
currentWindowIndex = player.getCurrentWindowIndex();
updateMediaSessionMetadata();
}
@@ -615,6 +619,13 @@ public final class MediaSessionConnector {
updateMediaSessionPlaybackState();
}
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL
+ : PlaybackStateCompat.SHUFFLE_MODE_NONE);
+ updateMediaSessionPlaybackState();
+ }
+
@Override
public void onPlayerError(ExoPlaybackException error) {
playbackException = error;
@@ -796,6 +807,14 @@ public final class MediaSessionConnector {
}
}
+ @Override
+ public void onSetShuffleMode(int shuffleMode) {
+ if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) {
+ queueNavigator.onSetShuffleModeEnabled(player,
+ shuffleMode != PlaybackStateCompat.SHUFFLE_MODE_NONE);
+ }
+ }
+
@Override
public void onAddQueueItem(MediaDescriptionCompat description) {
if (queueEditor != null) {
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
index abefe533ce..db0190de0f 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession;
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import android.content.Context;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
index 521b4cd6e3..435d994dcc 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -163,7 +163,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
@Override
public void onSetShuffleModeEnabled(Player player, boolean enabled) {
- // TODO: Implement this.
+ player.setShuffleModeEnabled(enabled);
}
private void publishFloatingQueueWindow(Player player) {
diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md
index b10c4ba629..f84d0c35f2 100644
--- a/extensions/okhttp/README.md
+++ b/extensions/okhttp/README.md
@@ -1,7 +1,5 @@
# ExoPlayer OkHttp extension #
-## Description ##
-
The OkHttp extension is an [HttpDataSource][] implementation using Square's
[OkHttp][].
@@ -49,3 +47,10 @@ new DefaultDataSourceFactory(
new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */);
```
respectively.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*`
+ belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/extensions/opus/README.md b/extensions/opus/README.md
index e5f5bcb168..d766e8c9c4 100644
--- a/extensions/opus/README.md
+++ b/extensions/opus/README.md
@@ -1,19 +1,16 @@
# ExoPlayer Opus extension #
-## Description ##
-
-The Opus extension is a [Renderer][] implementation that helps you bundle
-libopus (the Opus decoding library) into your app and use it along with
-ExoPlayer to play Opus audio on Android devices.
-
-[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
+The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus
+decoding library) to decode Opus audio.
## Build instructions ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
-[top level README][]. In addition, it's necessary to build the extension's
-native components as follows:
+[top level README][].
+
+In addition, it's necessary to build the extension's native components as
+follows:
* Set the following environment variables:
@@ -59,3 +56,38 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
* Clean and re-build the project.
* If you want to use your own version of libopus, place it in
`${OPUS_EXT_PATH}/jni/libopus`.
+
+## Using the extension ##
+
+Once you've followed the instructions above to check out, build and depend on
+the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`.
+How you do this depends on which player API you're using:
+
+* If you're passing a `DefaultRenderersFactory` to
+ `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
+ setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
+ constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
+ `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
+ support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
+ `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`.
+* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer`
+ to the output list in `buildAudioRenderers`. ExoPlayer will use the first
+ `Renderer` in the list that supports the input media format.
+* If you've implemented your own `RenderersFactory`, return a
+ `LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
+ first `Renderer` in the returned array that supports the input media format.
+* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer`
+ in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
+ list that supports the input media format.
+
+Note: These instructions assume you're using `DefaultTrackSelector`. If you have
+a custom track selector the choice of `Renderer` is up to your implementation,
+so you need to make sure you are passing an `LibopusAudioRenderer` to the
+player, then implement your own logic to use the renderer for a given track.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*`
+ belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index e77590dc65..aba71a0821 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.opus.test">
-
+
-
+
-
diff --git a/library/core/README.md b/library/core/README.md
new file mode 100644
index 0000000000..f31ffed131
--- /dev/null
+++ b/library/core/README.md
@@ -0,0 +1,9 @@
+# ExoPlayer core library module #
+
+The core of the ExoPlayer library.
+
+## Links ##
+
+* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml
index aeddc611cf..4997994e18 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.core.test">
-
+ 00:00:01,234
+This is the first subtitle.
+
+2
+00:00:02,345 --> 00:00:03,456
+This is the second subtitle.
+Second subtitle with second line.
+
+3
\ No newline at end of file
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
index bf4ea6e972..bc72ebc060 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -15,20 +15,18 @@
*/
package com.google.android.exoplayer2;
-import android.util.Pair;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
+import com.google.android.exoplayer2.testutil.ActionSchedule;
+import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
+import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
-import com.google.android.exoplayer2.util.MimeTypes;
-import java.util.LinkedList;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import junit.framework.TestCase;
/**
@@ -43,67 +41,59 @@ public final class ExoPlayerTest extends TestCase {
*/
private static final int TIMEOUT_MS = 10000;
- private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null,
- MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
- null, null);
- private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null,
- MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
-
/**
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without
* error.
*/
public void testPlayEmptyTimeline() throws Exception {
- ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = Timeline.EMPTY;
- MediaSource mediaSource = new FakeMediaSource(timeline, null);
FakeRenderer renderer = new FakeRenderer();
- playerWrapper.setup(mediaSource, renderer);
- playerWrapper.blockUntilEnded(TIMEOUT_MS);
- assertEquals(0, playerWrapper.positionDiscontinuityCount);
+ ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline).setRenderers(renderer)
+ .build().start().blockUntilEnded(TIMEOUT_MS);
+ testRunner.assertPositionDiscontinuityCount(0);
+ testRunner.assertTimelinesEqual(timeline);
assertEquals(0, renderer.formatReadCount);
assertEquals(0, renderer.bufferReadCount);
assertFalse(renderer.isEnded);
- playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
}
/**
* Tests playback of a source that exposes a single period.
*/
public void testPlaySinglePeriodTimeline() throws Exception {
- ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Object manifest = new Object();
- MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
- FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
- playerWrapper.setup(mediaSource, renderer);
- playerWrapper.blockUntilEnded(TIMEOUT_MS);
- assertEquals(0, playerWrapper.positionDiscontinuityCount);
+ FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
+ ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline).setManifest(manifest).setRenderers(renderer)
+ .build().start().blockUntilEnded(TIMEOUT_MS);
+ testRunner.assertPositionDiscontinuityCount(0);
+ testRunner.assertTimelinesEqual(timeline);
+ testRunner.assertManifestsEqual(manifest);
+ testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
assertEquals(1, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded);
- assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
- playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest));
}
/**
* Tests playback of a source that exposes three periods.
*/
public void testPlayMultiPeriodTimeline() throws Exception {
- ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0));
- MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
- FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
- playerWrapper.setup(mediaSource, renderer);
- playerWrapper.blockUntilEnded(TIMEOUT_MS);
- assertEquals(2, playerWrapper.positionDiscontinuityCount);
+ FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
+ ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline).setRenderers(renderer)
+ .build().start().blockUntilEnded(TIMEOUT_MS);
+ testRunner.assertPositionDiscontinuityCount(2);
+ testRunner.assertTimelinesEqual(timeline);
assertEquals(3, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded);
- playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
}
/**
@@ -111,16 +101,12 @@ public final class ExoPlayerTest extends TestCase {
* source.
*/
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
- final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10));
- MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT,
- TEST_AUDIO_FORMAT);
-
- FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT);
- FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) {
+ final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
+ FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
@Override
public long getPositionUs() {
@@ -143,35 +129,30 @@ public final class ExoPlayerTest extends TestCase {
@Override
public boolean isEnded() {
- // Allow playback to end once the final period is playing.
- return playerWrapper.positionDiscontinuityCount == 2;
+ return videoRenderer.isEnded();
}
};
- playerWrapper.setup(mediaSource, videoRenderer, audioRenderer);
- playerWrapper.blockUntilEnded(TIMEOUT_MS);
- assertEquals(2, playerWrapper.positionDiscontinuityCount);
+ ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline).setRenderers(videoRenderer, audioRenderer)
+ .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)
+ .build().start().blockUntilEnded(TIMEOUT_MS);
+ testRunner.assertPositionDiscontinuityCount(2);
+ testRunner.assertTimelinesEqual(timeline);
assertEquals(1, audioRenderer.positionResetCount);
assertTrue(videoRenderer.isEnded);
assertTrue(audioRenderer.isEnded);
- playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
}
public void testRepreparationGivesFreshSourceInfo() throws Exception {
- ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
- FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
-
- // Prepare the player with a source with the first manifest and a non-empty timeline
+ FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
Object firstSourceManifest = new Object();
- playerWrapper.setup(new FakeMediaSource(timeline, firstSourceManifest, TEST_VIDEO_FORMAT),
- renderer);
- playerWrapper.blockUntilSourceInfoRefreshed(TIMEOUT_MS);
-
- // Prepare the player again with a source and a new manifest, which will never be exposed.
+ MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
+ Builder.VIDEO_FORMAT);
final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);
final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);
- playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) {
+ MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) {
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
super.prepareSource(player, isTopLevelSource, listener);
@@ -185,29 +166,49 @@ public final class ExoPlayerTest extends TestCase {
throw new IllegalStateException(e);
}
}
- });
-
- // Prepare the player again with a third source.
- queuedSourceInfoCountDownLatch.await();
+ };
Object thirdSourceManifest = new Object();
- playerWrapper.prepare(new FakeMediaSource(timeline, thirdSourceManifest, TEST_VIDEO_FORMAT));
- completePreparationCountDownLatch.countDown();
-
- // Wait for playback to complete.
- playerWrapper.blockUntilEnded(TIMEOUT_MS);
- assertEquals(0, playerWrapper.positionDiscontinuityCount);
- assertEquals(1, renderer.formatReadCount);
- assertEquals(1, renderer.bufferReadCount);
- assertTrue(renderer.isEnded);
- assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
+ MediaSource thirdSource = new FakeMediaSource(timeline, thirdSourceManifest,
+ Builder.VIDEO_FORMAT);
+ // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare
+ // the player again with a source and a new manifest, which will never be exposed. Allow the
+ // test thread to prepare the player with a third source, and block the playback thread until
+ // the test thread's call to prepare() has returned.
+ ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation")
+ .waitForTimelineChanged(timeline)
+ .prepareSource(secondSource)
+ .executeRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ queuedSourceInfoCountDownLatch.await();
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+ })
+ .prepareSource(thirdSource)
+ .executeRunnable(new Runnable() {
+ @Override
+ public void run() {
+ completePreparationCountDownLatch.countDown();
+ }
+ })
+ .build();
+ ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
+ .setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule)
+ .build().start().blockUntilEnded(TIMEOUT_MS);
+ testRunner.assertPositionDiscontinuityCount(0);
// The first source's preparation completed with a non-empty timeline. When the player was
// re-prepared with the second source, it immediately exposed an empty timeline, but the source
// info refresh from the second source was suppressed as we re-prepared with the third source.
- playerWrapper.assertSourceInfosEquals(
- Pair.create(timeline, firstSourceManifest),
- Pair.create(Timeline.EMPTY, null),
- Pair.create(timeline, thirdSourceManifest));
+ testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);
+ testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest);
+ testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
+ assertEquals(1, renderer.formatReadCount);
+ assertEquals(1, renderer.bufferReadCount);
+ assertTrue(renderer.isEnded);
}
public void testRepeatModeChanges() throws Exception {
@@ -215,49 +216,22 @@ public final class ExoPlayerTest extends TestCase {
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000));
- final int[] actionSchedule = { // 0 -> 1
- Player.REPEAT_MODE_ONE, // 1 -> 1
- Player.REPEAT_MODE_OFF, // 1 -> 2
- Player.REPEAT_MODE_ONE, // 2 -> 2
- Player.REPEAT_MODE_ALL, // 2 -> 0
- Player.REPEAT_MODE_ONE, // 0 -> 0
- -1, // 0 -> 0
- Player.REPEAT_MODE_OFF, // 0 -> 1
- -1, // 1 -> 2
- -1 // 2 -> ended
- };
- int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
- final LinkedList windowIndices = new LinkedList<>();
- final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
- ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
- @Override
- @SuppressWarnings("ResourceType")
- public void onPositionDiscontinuity() {
- super.onPositionDiscontinuity();
- int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
- if (actionSchedule[actionIndex] != -1) {
- player.setRepeatMode(actionSchedule[actionIndex]);
- }
- windowIndices.add(player.getCurrentWindowIndex());
- actionCounter.countDown();
- }
- };
- MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
- FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
- playerWrapper.setup(mediaSource, renderer);
- boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- playerWrapper.release();
- assertTrue("Test playback timed out waiting for action schedule to end.", finished);
- if (playerWrapper.exception != null) {
- throw playerWrapper.exception;
- }
- assertEquals(expectedWindowIndices.length, windowIndices.size());
- for (int i = 0; i < expectedWindowIndices.length; i++) {
- assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
- }
- assertEquals(9, playerWrapper.positionDiscontinuityCount);
+ FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
+ ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1
+ .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1
+ .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2
+ .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 2 -> 2
+ .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ALL) // 2 -> 0
+ .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 0 -> 0
+ .waitForPositionDiscontinuity() // 0 -> 0
+ .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 0 -> end
+ .build();
+ ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule)
+ .build().start().blockUntilEnded(TIMEOUT_MS);
+ testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2);
+ testRunner.assertTimelinesEqual(timeline);
assertTrue(renderer.isEnded);
- playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
index 9f5b067b5e..43c867f435 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.google.android.exoplayer2.drm;
import static org.mockito.Matchers.any;
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
index 76c13495c1..c9364aa605 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase;
-import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
@@ -38,11 +37,6 @@ public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
}
- public void testAtomWithZeroSize() throws Exception {
- ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
- getInstrumentation(), ParserException.class);
- }
-
private static ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0);
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java
new file mode 100644
index 0000000000..5ac3979746
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.extractor.mp4;
+
+import android.test.MoreAsserts;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.UUID;
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link PsshAtomUtil}.
+ */
+public class PsshAtomUtilTest extends TestCase {
+
+ public void testBuildPsshAtom() {
+ byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5};
+ byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData);
+ // Read the PSSH atom back and assert its content is as expected.
+ ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom);
+ assertEquals(psshAtom.length, parsablePsshAtom.readUnsignedIntToInt()); // length
+ assertEquals(Atom.TYPE_pssh, parsablePsshAtom.readInt()); // type
+ int fullAtomInt = parsablePsshAtom.readInt(); // version + flags
+ assertEquals(0, Atom.parseFullAtomVersion(fullAtomInt));
+ assertEquals(0, Atom.parseFullAtomFlags(fullAtomInt));
+ UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong());
+ assertEquals(C.WIDEVINE_UUID, systemId);
+ assertEquals(schemeData.length, parsablePsshAtom.readUnsignedIntToInt());
+ byte[] psshSchemeData = new byte[schemeData.length];
+ parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length);
+ MoreAsserts.assertEquals(schemeData, psshSchemeData);
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java
index d52deb108f..1acc208c29 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java
@@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
- new byte[]{'O', 'g', 'g', 'S'},
+ new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
@@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
- new byte[]{'O', 'g', 'g', 'S'},
+ new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
@@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
- new byte[]{'x', 'O', 'g', 'g', 'S'}
+ new byte[] {'x', 'O', 'g', 'g', 'S'}
), false);
skipToNextPage(extractorInput);
assertEquals(1, extractorInput.getPosition());
@@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
- new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false);
+ new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
try {
skipToNextPage(extractorInput);
fail();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java
index bcfa90a565..6a31250e15 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
@@ -154,20 +155,20 @@ public class AdtsReaderTest extends TestCase {
}
}
- public void testAdtsDataOnly() throws Exception {
+ public void testAdtsDataOnly() throws ParserException {
data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length);
feed();
assertSampleCounts(0, 1);
adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null);
}
- private void feedLimited(int limit) {
+ private void feedLimited(int limit) throws ParserException {
maybeStartPacket();
data.setLimit(limit);
feed();
}
- private void feed() {
+ private void feed() throws ParserException {
maybeStartPacket();
adtsReader.consume(data);
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java
new file mode 100644
index 0000000000..ec45ea01c7
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.offline;
+
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.DummyDataSource;
+import com.google.android.exoplayer2.upstream.cache.Cache;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import org.mockito.Mockito;
+
+/**
+ * Unit tests for {@link ProgressiveDownloadAction}.
+ */
+public class ProgressiveDownloadActionTest extends InstrumentationTestCase {
+
+ public void testDownloadActionIsNotRemoveAction() throws Exception {
+ ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
+ assertFalse(action.isRemoveAction());
+ }
+
+ public void testRemoveActionIsRemoveAction() throws Exception {
+ ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true);
+ assertTrue(action2.isRemoveAction());
+ }
+
+ public void testCreateDownloader() throws Exception {
+ TestUtil.setUpMockito(this);
+ ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
+ DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(
+ Mockito.mock(Cache.class), DummyDataSource.FACTORY);
+ assertNotNull(action.createDownloader(constructorHelper));
+ }
+
+ public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception {
+ ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true);
+ ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false);
+ assertTrue(action1.isSameMedia(action2));
+ }
+
+ public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception {
+ ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true);
+ ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false);
+ assertFalse(action3.isSameMedia(action4));
+ }
+
+ public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception {
+ ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true);
+ ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false);
+ assertTrue(action5.isSameMedia(action6));
+ }
+
+ public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception {
+ ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true);
+ ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false);
+ assertFalse(action7.isSameMedia(action8));
+ }
+
+ public void testEquals() throws Exception {
+ ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true);
+ assertTrue(action1.equals(action1));
+
+ ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true);
+ ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true);
+ assertTrue(action2.equals(action3));
+
+ ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true);
+ ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false);
+ assertFalse(action4.equals(action5));
+
+ ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true);
+ ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true);
+ assertFalse(action6.equals(action7));
+
+ ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true);
+ ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true);
+ assertFalse(action8.equals(action9));
+
+ ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true);
+ ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true);
+ assertFalse(action10.equals(action11));
+ }
+
+ public void testSerializerGetType() throws Exception {
+ ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false);
+ assertNotNull(action.getType());
+ }
+
+ public void testSerializerWriteRead() throws Exception {
+ doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false));
+ doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true));
+ }
+
+ private void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputStream output = new DataOutputStream(out);
+ action1.writeToStream(output);
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+ DataInputStream input = new DataInputStream(in);
+ DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input);
+
+ assertEquals(action1, action2);
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
index f8636b9990..8d29a95d89 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
@@ -396,6 +396,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
throw new UnsupportedOperationException();
}
+ @Override
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getShuffleModeEnabled() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public boolean isLoading() {
throw new UnsupportedOperationException();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
index 76ea0e34cf..77e61e39a9 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -258,45 +258,45 @@ public class SampleQueueTest extends TestCase {
public void testAdvanceToBeforeBuffer() {
writeTestData();
- boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
+ int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
// Should fail and have no effect.
- assertFalse(result);
+ assertEquals(SampleQueue.ADVANCE_FAILED, skipCount);
assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToStartOfBuffer() {
writeTestData();
- boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
+ int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
// Should succeed but have no effect (we're already at the first frame).
- assertTrue(result);
+ assertEquals(0, skipCount);
assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToEndOfBuffer() {
writeTestData();
- boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
- // Should succeed and skip to 2nd keyframe.
- assertTrue(result);
+ int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
+ // Should succeed and skip to 2nd keyframe (the 4th frame).
+ assertEquals(4, skipCount);
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToAfterBuffer() {
writeTestData();
- boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
+ int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
// Should fail and have no effect.
- assertFalse(result);
+ assertEquals(SampleQueue.ADVANCE_FAILED, skipCount);
assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2);
}
public void testAdvanceToAfterBufferAllowed() {
writeTestData();
- boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
- // Should succeed and skip to 2nd keyframe.
- assertTrue(result);
+ int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
+ // Should succeed and skip to 2nd keyframe (the 4th frame).
+ assertEquals(4, skipCount);
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
assertNoSamplesToRead(TEST_FORMAT_2);
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java
new file mode 100644
index 0000000000..5de6bdf3e1
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
+import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;
+import junit.framework.TestCase;
+
+/**
+ * Unit test for {@link ShuffleOrder}.
+ */
+public final class ShuffleOrderTest extends TestCase {
+
+ public static final long RANDOM_SEED = 1234567890L;
+
+ public void testDefaultShuffleOrder() {
+ assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0);
+ assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1);
+ assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5);
+ for (int initialLength = 0; initialLength < 4; initialLength++) {
+ for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
+ testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0);
+ testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1);
+ testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);
+ }
+ }
+ testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0);
+ testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2);
+ testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4);
+ testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0);
+ }
+
+ public void testUnshuffledShuffleOrder() {
+ assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0);
+ assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1);
+ assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5);
+ for (int initialLength = 0; initialLength < 4; initialLength++) {
+ for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
+ testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0);
+ testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1);
+ testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);
+ }
+ }
+ testCloneAndRemove(new UnshuffledShuffleOrder(5), 0);
+ testCloneAndRemove(new UnshuffledShuffleOrder(5), 2);
+ testCloneAndRemove(new UnshuffledShuffleOrder(5), 4);
+ testCloneAndRemove(new UnshuffledShuffleOrder(1), 0);
+ }
+
+ public void testUnshuffledShuffleOrderIsUnshuffled() {
+ ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5);
+ assertEquals(0, shuffleOrder.getFirstIndex());
+ assertEquals(4, shuffleOrder.getLastIndex());
+ for (int i = 0; i < 4; i++) {
+ assertEquals(i + 1, shuffleOrder.getNextIndex(i));
+ }
+ }
+
+ private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) {
+ assertEquals(length, shuffleOrder.getLength());
+ if (length == 0) {
+ assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex());
+ assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex());
+ } else {
+ int[] indices = new int[length];
+ indices[0] = shuffleOrder.getFirstIndex();
+ assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0]));
+ for (int i = 1; i < length; i++) {
+ indices[i] = shuffleOrder.getNextIndex(indices[i - 1]);
+ assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i]));
+ for (int j = 0; j < i; j++) {
+ assertTrue(indices[i] != indices[j]);
+ }
+ }
+ assertEquals(indices[length - 1], shuffleOrder.getLastIndex());
+ assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1]));
+ for (int i = 0; i < length; i++) {
+ assertTrue(indices[i] >= 0 && indices[i] < length);
+ }
+ }
+ }
+
+ private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) {
+ ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count);
+ assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count);
+ // Assert all elements still have the relative same order
+ for (int i = 0; i < shuffleOrder.getLength(); i++) {
+ int expectedNextIndex = shuffleOrder.getNextIndex(i);
+ if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
+ expectedNextIndex += count;
+ }
+ int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count);
+ while (newNextIndex >= position && newNextIndex < position + count) {
+ newNextIndex = newOrder.getNextIndex(newNextIndex);
+ }
+ assertEquals(expectedNextIndex, newNextIndex);
+ }
+ }
+
+ private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) {
+ ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position);
+ assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1);
+ // Assert all elements still have the relative same order
+ for (int i = 0; i < shuffleOrder.getLength(); i++) {
+ if (i == position) {
+ continue;
+ }
+ int expectedNextIndex = shuffleOrder.getNextIndex(i);
+ if (expectedNextIndex == position) {
+ expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);
+ }
+ if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
+ expectedNextIndex--;
+ }
+ int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1);
+ assertEquals(expectedNextIndex, newNextIndex);
+ }
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
index 167499fcdc..744634edda 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
@@ -31,6 +31,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode";
private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence";
private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps";
+ private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end";
private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes";
public void testDecodeEmpty() throws IOException {
@@ -107,6 +108,17 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
assertTypicalCue3(subtitle, 0);
}
+ public void testDecodeTypicalUnexpectedEnd() throws IOException {
+ // Parsing should succeed, parsing the first and second cues only.
+ SubripDecoder decoder = new SubripDecoder();
+ byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_UNEXPECTED_END);
+ SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertEquals(4, subtitle.getEventTimeCount());
+ assertTypicalCue1(subtitle, 0);
+ assertTypicalCue2(subtitle, 2);
+ }
+
public void testDecodeNoEndTimecodes() throws IOException {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java
new file mode 100644
index 0000000000..5ba9e18e7d
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import java.io.IOException;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link DataSchemeDataSource}.
+ */
+public final class DataSchemeDataSourceTest extends TestCase {
+
+ private DataSource schemeDataDataSource;
+
+ @Override
+ public void setUp() {
+ schemeDataDataSource = new DataSchemeDataSource();
+ }
+
+ public void testBase64Data() throws IOException {
+ DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL"
+ + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM"
+ + "DAwMDAwMDAwMDAiXX0=");
+ TestUtil.assertDataSourceContent(schemeDataDataSource, dataSpec,
+ ("{\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":"
+ + "[\"00000000000000000000000000000000\"]}").getBytes());
+ }
+
+ public void testAsciiData() throws IOException {
+ TestUtil.assertDataSourceContent(schemeDataDataSource, buildDataSpec("data:,A%20brief%20note"),
+ "A brief note".getBytes());
+ }
+
+ public void testPartialReads() throws IOException {
+ byte[] buffer = new byte[18];
+ DataSpec dataSpec = buildDataSpec("data:,012345678901234567");
+ assertEquals(18, schemeDataDataSource.open(dataSpec));
+ assertEquals(9, schemeDataDataSource.read(buffer, 0, 9));
+ assertEquals(0, schemeDataDataSource.read(buffer, 3, 0));
+ assertEquals(9, schemeDataDataSource.read(buffer, 9, 15));
+ assertEquals(0, schemeDataDataSource.read(buffer, 1, 0));
+ assertEquals(C.RESULT_END_OF_INPUT, schemeDataDataSource.read(buffer, 1, 1));
+ assertEquals("012345678901234567", new String(buffer, 0, 18));
+ }
+
+ public void testIncorrectScheme() {
+ try {
+ schemeDataDataSource.open(buildDataSpec("http://www.google.com"));
+ fail();
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ public void testMalformedData() {
+ try {
+ schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,,This%20is%20Content"));
+ fail();
+ } catch (IOException e) {
+ // Expected.
+ }
+ try {
+ schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,IncorrectPadding=="));
+ fail();
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ private static DataSpec buildDataSpec(String uriString) {
+ return new DataSpec(Uri.parse(uriString));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java
index 6c5d7c76f7..b4f1d50293 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase;
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java
index cfb9cd78be..d7b2b36740 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java
@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.util;
import android.test.MoreAsserts;
-
import junit.framework.TestCase;
/**
@@ -27,8 +26,14 @@ public final class ParsableBitArrayTest extends TestCase {
private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01,
(byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99};
+ private ParsableBitArray testArray;
+
+ @Override
+ public void setUp() {
+ testArray = new ParsableBitArray(TEST_DATA);
+ }
+
public void testReadAllBytes() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
byte[] bytesRead = new byte[TEST_DATA.length];
testArray.readBytes(bytesRead, 0, TEST_DATA.length);
MoreAsserts.assertEquals(TEST_DATA, bytesRead);
@@ -37,13 +42,12 @@ public final class ParsableBitArrayTest extends TestCase {
}
public void testReadBit() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
- assertReadBitsToEnd(0, testArray);
+ assertReadBitsToEnd(0);
}
public void testReadBits() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
assertEquals(getTestDataBits(0, 5), testArray.readBits(5));
+ assertEquals(getTestDataBits(5, 0), testArray.readBits(0));
assertEquals(getTestDataBits(5, 3), testArray.readBits(3));
assertEquals(getTestDataBits(8, 16), testArray.readBits(16));
assertEquals(getTestDataBits(24, 3), testArray.readBits(3));
@@ -52,67 +56,101 @@ public final class ParsableBitArrayTest extends TestCase {
assertEquals(getTestDataBits(50, 14), testArray.readBits(14));
}
+ public void testReadBitsToByteArray() {
+ byte[] result = new byte[TEST_DATA.length];
+ // Test read within byte boundaries.
+ testArray.readBits(result, 0, 6);
+ assertEquals(TEST_DATA[0] & 0xFC, result[0]);
+ // Test read across byte boundaries.
+ testArray.readBits(result, 0, 8);
+ assertEquals(((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2), result[0]);
+ // Test reading across multiple bytes.
+ testArray.readBits(result, 1, 50);
+ for (int i = 1; i < 7; i++) {
+ assertEquals((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)),
+ result[i]);
+ }
+ assertEquals((byte) (TEST_DATA[7] & 0x03) << 6, result[7]);
+ assertEquals(0, testArray.bitsLeft());
+ // Test read last buffer byte across input data bytes.
+ testArray.setPosition(31);
+ result[3] = 0;
+ testArray.readBits(result, 3, 3);
+ assertEquals((byte) 0xE0, result[3]);
+ // Test read bits in the middle of a input data byte.
+ result[0] = 0;
+ assertEquals(34, testArray.getPosition());
+ testArray.readBits(result, 0, 3);
+ assertEquals((byte) 0xE0, result[0]);
+ // Test read 0 bits.
+ testArray.setPosition(32);
+ result[1] = 0;
+ testArray.readBits(result, 1, 0);
+ assertEquals(0, result[1]);
+ // Test reading a number of bits divisible by 8.
+ testArray.setPosition(0);
+ testArray.readBits(result, 0, 16);
+ assertEquals(TEST_DATA[0], result[0]);
+ assertEquals(TEST_DATA[1], result[1]);
+ // Test least significant bits are unmodified.
+ result[1] = (byte) 0xFF;
+ testArray.readBits(result, 0, 9);
+ assertEquals(0x5F, result[0]);
+ assertEquals(0x7F, result[1]);
+ }
+
public void testRead32BitsByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
assertEquals(getTestDataBits(0, 32), testArray.readBits(32));
assertEquals(getTestDataBits(32, 32), testArray.readBits(32));
}
public void testRead32BitsNonByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
assertEquals(getTestDataBits(0, 5), testArray.readBits(5));
assertEquals(getTestDataBits(5, 32), testArray.readBits(32));
}
public void testSkipBytes() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.skipBytes(2);
- assertReadBitsToEnd(16, testArray);
+ assertReadBitsToEnd(16);
}
public void testSkipBitsByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.skipBits(16);
- assertReadBitsToEnd(16, testArray);
+ assertReadBitsToEnd(16);
}
public void testSkipBitsNonByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.skipBits(5);
- assertReadBitsToEnd(5, testArray);
+ assertReadBitsToEnd(5);
}
public void testSetPositionByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(16);
- assertReadBitsToEnd(16, testArray);
+ assertReadBitsToEnd(16);
}
public void testSetPositionNonByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(5);
- assertReadBitsToEnd(5, testArray);
+ assertReadBitsToEnd(5);
}
public void testByteAlignFromNonByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(11);
testArray.byteAlign();
assertEquals(2, testArray.getBytePosition());
assertEquals(16, testArray.getPosition());
- assertReadBitsToEnd(16, testArray);
+ assertReadBitsToEnd(16);
}
public void testByteAlignFromByteAligned() {
- ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(16);
testArray.byteAlign(); // Should be a no-op.
assertEquals(2, testArray.getBytePosition());
assertEquals(16, testArray.getPosition());
- assertReadBitsToEnd(16, testArray);
+ assertReadBitsToEnd(16);
}
- private static void assertReadBitsToEnd(int expectedStartPosition, ParsableBitArray testArray) {
+ private void assertReadBitsToEnd(int expectedStartPosition) {
int position = testArray.getPosition();
assertEquals(expectedStartPosition, position);
for (int i = position; i < TEST_DATA.length * 8; i++) {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java
index 49719b95f7..324d668c7a 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java
@@ -279,7 +279,7 @@ public class ParsableByteArrayTest extends TestCase {
}
public void testReadLittleEndianLong() {
- ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
+ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, (byte) 0xFF
});
@@ -296,7 +296,7 @@ public class ParsableByteArrayTest extends TestCase {
}
public void testReadLittleEndianInt() {
- ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
+ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, 0x00, 0x00, (byte) 0xFF
});
assertEquals(0xFF000001, byteArray.readLittleEndianInt());
@@ -311,7 +311,7 @@ public class ParsableByteArrayTest extends TestCase {
}
public void testReadLittleEndianUnsignedShort() {
- ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
+ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, (byte) 0xFF, 0x02, (byte) 0xFF
});
assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort());
@@ -321,7 +321,7 @@ public class ParsableByteArrayTest extends TestCase {
}
public void testReadLittleEndianShort() {
- ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
+ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, (byte) 0xFF, 0x02, (byte) 0xFF
});
assertEquals((short) 0xFF01, byteArray.readLittleEndianShort());
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index a88a1dd615..7f14837965 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -296,9 +296,10 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* {@code positionUs} is beyond it.
*
* @param positionUs The position in microseconds.
+ * @return The number of samples that were skipped.
*/
- protected void skipSource(long positionUs) {
- stream.skipData(positionUs - streamOffsetUs);
+ protected int skipSource(long positionUs) {
+ return stream.skipData(positionUs - streamOffsetUs);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index d7d0ed40aa..e25538a062 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -73,6 +73,10 @@ public final class C {
*/
public static final long NANOS_PER_SECOND = 1000000000L;
+ /**
+ * The name of the ASCII charset.
+ */
+ public static final String ASCII_NAME = "US-ASCII";
/**
* The name of the UTF-8 charset.
*/
@@ -604,12 +608,19 @@ public final class C {
*/
public static final UUID UUID_NIL = new UUID(0L, 0L);
+ /**
+ * UUID for the W3C
+ * Common PSSH
+ * box.
+ */
+ public static final UUID COMMON_PSSH_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
+
/**
* UUID for the ClearKey DRM scheme.
*
* ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
*/
- public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
+ public static final UUID CLEARKEY_UUID = new UUID(0xE2719D58A985B3C9L, 0x781AB030AF78D30EL);
/**
* UUID for the Widevine DRM scheme.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index 3e7cb8a68b..2272306117 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -19,6 +19,7 @@ import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -27,7 +28,9 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
@@ -79,7 +82,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
private final Context context;
- private final DrmSessionManager drmSessionManager;
+ @Nullable private final DrmSessionManager drmSessionManager;
private final @ExtensionRendererMode int extensionRendererMode;
private final long allowedVideoJoiningTimeMs;
@@ -96,29 +99,28 @@ public class DefaultRenderersFactory implements RenderersFactory {
* playbacks are not required.
*/
public DefaultRenderersFactory(Context context,
- DrmSessionManager drmSessionManager) {
+ @Nullable DrmSessionManager drmSessionManager) {
this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF);
}
/**
* @param context A {@link Context}.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
- * playbacks are not required..
+ * playbacks are not required.
* @param extensionRendererMode The extension renderer mode, which determines if and how
* available extension renderers are used. Note that extensions must be included in the
* application build for them to be considered available.
*/
public DefaultRenderersFactory(Context context,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
@ExtensionRendererMode int extensionRendererMode) {
- this(context, drmSessionManager, extensionRendererMode,
- DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
+ this(context, drmSessionManager, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
}
/**
* @param context A {@link Context}.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
- * playbacks are not required..
+ * playbacks are not required.
* @param extensionRendererMode The extension renderer mode, which determines if and how
* available extension renderers are used. Note that extensions must be included in the
* application build for them to be considered available.
@@ -126,7 +128,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
* to seamlessly join an ongoing playback.
*/
public DefaultRenderersFactory(Context context,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) {
this.context = context;
this.drmSessionManager = drmSessionManager;
@@ -138,7 +140,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
public Renderer[] createRenderers(Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
- TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) {
+ TextOutput textRendererOutput, MetadataOutput metadataRendererOutput) {
ArrayList renderersList = new ArrayList<>();
buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
@@ -166,9 +168,10 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param out An array to which the built renderers should be appended.
*/
protected void buildVideoRenderers(Context context,
- DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs,
- Handler eventHandler, VideoRendererEventListener eventListener,
- @ExtensionRendererMode int extensionRendererMode, ArrayList out) {
+ @Nullable DrmSessionManager drmSessionManager,
+ long allowedVideoJoiningTimeMs, Handler eventHandler,
+ VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
+ ArrayList out) {
out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT,
allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
@@ -211,7 +214,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param out An array to which the built renderers should be appended.
*/
protected void buildAudioRenderers(Context context,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
AudioProcessor[] audioProcessors, Handler eventHandler,
AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
ArrayList out) {
@@ -282,8 +285,8 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param extensionRendererMode The extension renderer mode.
* @param out An array to which the built renderers should be appended.
*/
- protected void buildTextRenderers(Context context, TextRenderer.Output output,
- Looper outputLooper, @ExtensionRendererMode int extensionRendererMode,
+ protected void buildTextRenderers(Context context, TextOutput output, Looper outputLooper,
+ @ExtensionRendererMode int extensionRendererMode,
ArrayList out) {
out.add(new TextRenderer(output, outputLooper));
}
@@ -298,9 +301,8 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param extensionRendererMode The extension renderer mode.
* @param out An array to which the built renderers should be appended.
*/
- protected void buildMetadataRenderers(Context context, MetadataRenderer.Output output,
- Looper outputLooper, @ExtensionRendererMode int extensionRendererMode,
- ArrayList out) {
+ protected void buildMetadataRenderers(Context context, MetadataOutput output, Looper outputLooper,
+ @ExtensionRendererMode int extensionRendererMode, ArrayList out) {
out.add(new MetadataRenderer(output, outputLooper));
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index 97a310c3da..b647e541bc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2;
import android.content.Context;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@@ -54,7 +55,8 @@ public final class ExoPlayerFactory {
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
- LoadControl loadControl, DrmSessionManager drmSessionManager) {
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager);
return newSimpleInstance(renderersFactory, trackSelector, loadControl);
}
@@ -74,7 +76,7 @@ public final class ExoPlayerFactory {
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
- LoadControl loadControl, DrmSessionManager drmSessionManager,
+ LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
extensionRendererMode);
@@ -98,7 +100,7 @@ public final class ExoPlayerFactory {
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
- LoadControl loadControl, DrmSessionManager drmSessionManager,
+ LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index c3a76cd962..0ce920a16f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -53,6 +53,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private boolean tracksSelected;
private boolean playWhenReady;
private @RepeatMode int repeatMode;
+ private boolean shuffleModeEnabled;
private int playbackState;
private int pendingSeekAcks;
private int pendingPrepareAcks;
@@ -87,6 +88,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.trackSelector = Assertions.checkNotNull(trackSelector);
this.playWhenReady = false;
this.repeatMode = Player.REPEAT_MODE_OFF;
+ this.shuffleModeEnabled = false;
this.playbackState = Player.STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>();
emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
@@ -105,7 +107,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
};
playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
- repeatMode, eventHandler, playbackInfo, this);
+ repeatMode, shuffleModeEnabled, eventHandler, playbackInfo, this);
}
@Override
@@ -189,6 +191,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
return repeatMode;
}
+ @Override
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ if (this.shuffleModeEnabled != shuffleModeEnabled) {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
+ for (Player.EventListener listener : listeners) {
+ listener.onShuffleModeEnabledChanged(shuffleModeEnabled);
+ }
+ }
+ }
+
+ @Override
+ public boolean getShuffleModeEnabled() {
+ return shuffleModeEnabled;
+ }
+
@Override
public boolean isLoading() {
return isLoading;
@@ -448,6 +466,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
case ExoPlayerImplInternal.MSG_SEEK_ACK: {
if (--pendingSeekAcks == 0) {
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+ if (timeline.isEmpty()) {
+ // Update the masking variables, which are used when the timeline is empty.
+ maskingPeriodIndex = 0;
+ maskingWindowIndex = 0;
+ maskingWindowPositionMs = 0;
+ }
if (msg.arg1 != 0) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity();
@@ -472,6 +496,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
timeline = sourceInfo.timeline;
manifest = sourceInfo.manifest;
playbackInfo = sourceInfo.playbackInfo;
+ if (pendingSeekAcks == 0 && timeline.isEmpty()) {
+ // Update the masking variables, which are used when the timeline is empty.
+ maskingPeriodIndex = 0;
+ maskingWindowIndex = 0;
+ maskingWindowPositionMs = 0;
+ }
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(timeline, manifest);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index a789dbc1b2..67586cc07a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -128,6 +128,7 @@ import java.io.IOException;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
private static final int MSG_CUSTOM = 11;
private static final int MSG_SET_REPEAT_MODE = 12;
+ private static final int MSG_SET_SHUFFLE_ENABLED = 13;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -173,6 +174,7 @@ import java.io.IOException;
private boolean isLoading;
private int state;
private @Player.RepeatMode int repeatMode;
+ private boolean shuffleModeEnabled;
private int customMessagesSent;
private int customMessagesProcessed;
private long elapsedRealtimeUs;
@@ -189,12 +191,14 @@ import java.io.IOException;
public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode,
- Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) {
+ boolean shuffleModeEnabled, Handler eventHandler, PlaybackInfo playbackInfo,
+ ExoPlayer player) {
this.renderers = renderers;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
this.playWhenReady = playWhenReady;
this.repeatMode = repeatMode;
+ this.shuffleModeEnabled = shuffleModeEnabled;
this.eventHandler = eventHandler;
this.state = Player.STATE_IDLE;
this.playbackInfo = playbackInfo;
@@ -234,6 +238,10 @@ import java.io.IOException;
handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget();
}
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget();
+ }
+
public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
.sendToTarget();
@@ -263,13 +271,18 @@ import java.io.IOException;
}
int messageNumber = customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+ boolean wasInterrupted = false;
while (customMessagesProcessed <= messageNumber) {
try {
wait();
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ wasInterrupted = true;
}
}
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
}
public synchronized void release() {
@@ -277,13 +290,18 @@ import java.io.IOException;
return;
}
handler.sendEmptyMessage(MSG_RELEASE);
+ boolean wasInterrupted = false;
while (!released) {
try {
wait();
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ wasInterrupted = true;
}
}
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
internalPlaybackThread.quit();
}
@@ -336,6 +354,10 @@ import java.io.IOException;
setRepeatModeInternal(msg.arg1);
return true;
}
+ case MSG_SET_SHUFFLE_ENABLED: {
+ setShuffleModeEnabledInternal(msg.arg1 != 0);
+ return true;
+ }
case MSG_DO_SOME_WORK: {
doSomeWork();
return true;
@@ -447,7 +469,17 @@ import java.io.IOException;
throws ExoPlaybackException {
this.repeatMode = repeatMode;
mediaPeriodInfoSequence.setRepeatMode(repeatMode);
+ validateExistingPeriodHolders();
+ }
+ private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
+ throws ExoPlaybackException {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ mediaPeriodInfoSequence.setShuffleModeEnabled(shuffleModeEnabled);
+ validateExistingPeriodHolders();
+ }
+
+ private void validateExistingPeriodHolders() throws ExoPlaybackException {
// Find the last existing period holder that matches the new period order.
MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null
? playingPeriodHolder : loadingPeriodHolder;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
index 4e387ac7ce..c6be2e2eba 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -428,28 +428,28 @@ public final class Format implements Parcelable {
}
public Format copyWithMaxInputSize(int maxInputSize) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
- width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
- stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
- encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, metadata);
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
}
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
- width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
- stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
- encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, metadata);
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
}
public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
@C.SelectionFlags int selectionFlags, String language) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
- width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
- stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
- encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, metadata);
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
}
@SuppressWarnings("ReferenceEquality")
@@ -474,27 +474,35 @@ public final class Format implements Parcelable {
}
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
- width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
- stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
- encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, metadata);
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
}
public Format copyWithDrmInitData(DrmInitData drmInitData) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
- width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
- stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
- encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, metadata);
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
}
public Format copyWithMetadata(Metadata metadata) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
- width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
- stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
- encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, metadata);
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
+ }
+
+ public Format copyWithRotationDegrees(int rotationDegrees) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
index 0e9c65421c..9e8c2645c1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
@@ -102,8 +102,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
private final Timeline.Window window;
private Timeline timeline;
- @RepeatMode
- private int repeatMode;
+ private @RepeatMode int repeatMode;
+ private boolean shuffleModeEnabled;
/**
* Creates a new media period info sequence.
@@ -129,6 +129,14 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
this.repeatMode = repeatMode;
}
+ /**
+ * Sets whether shuffling is enabled. Call {@link #getUpdatedMediaPeriodInfo} to update period
+ * information taking into account the shuffle mode.
+ */
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ }
+
/**
* Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
index d2480c5b3a..6eee930018 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -94,6 +94,13 @@ public interface Player {
*/
void onRepeatModeChanged(@RepeatMode int repeatMode);
+ /**
+ * Called when the value of {@link #getShuffleModeEnabled()} changes.
+ *
+ * @param shuffleModeEnabled Whether shuffling of windows is enabled.
+ */
+ void onShuffleModeEnabledChanged(boolean shuffleModeEnabled);
+
/**
* Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
* immediately after this method is called. The player instance can still be used, and
@@ -219,6 +226,18 @@ public interface Player {
*/
@RepeatMode int getRepeatMode();
+ /**
+ * Sets whether shuffling of windows is enabled.
+ *
+ * @param shuffleModeEnabled Whether shuffling is enabled.
+ */
+ void setShuffleModeEnabled(boolean shuffleModeEnabled);
+
+ /**
+ * Returns whether shuffling of windows is enabled.
+ */
+ boolean getShuffleModeEnabled();
+
/**
* Whether the player is currently loading the source.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java
index f841a1b8b5..3f1be20cfb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java
@@ -24,7 +24,7 @@ public interface RendererCapabilities {
/**
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
- * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES},
+ * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM},
* {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
*/
int FORMAT_SUPPORT_MASK = 0b111;
@@ -117,8 +117,8 @@ public interface RendererCapabilities {
* the bitwise OR of three properties:
*
*
The level of support for the format itself. One of {@link #FORMAT_HANDLED},
- * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
- * {@link #FORMAT_UNSUPPORTED_TYPE}.
The level of support for adapting from the format to another format of the same mime type.
* One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
* {@link #ADAPTIVE_NOT_SUPPORTED}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java
index 728cfa387a..a08ba448a4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2;
import android.os.Handler;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
-import com.google.android.exoplayer2.metadata.MetadataRenderer;
-import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
+import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
/**
@@ -38,7 +38,7 @@ public interface RenderersFactory {
*/
Renderer[] createRenderers(Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
- AudioRendererEventListener audioRendererEventListener,
- TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput);
+ AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 3a3768bcc2..9fcc4d2128 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -31,16 +31,17 @@ import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.metadata.Metadata;
-import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
-import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
/**
* An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
@@ -87,6 +88,9 @@ public class SimpleExoPlayer implements ExoPlayer {
private final ExoPlayer player;
private final ComponentListener componentListener;
+ private final CopyOnWriteArraySet videoListeners;
+ private final CopyOnWriteArraySet textOutputs;
+ private final CopyOnWriteArraySet metadataOutputs;
private final int videoRendererCount;
private final int audioRendererCount;
@@ -99,9 +103,6 @@ public class SimpleExoPlayer implements ExoPlayer {
private int videoScalingMode;
private SurfaceHolder surfaceHolder;
private TextureView textureView;
- private TextRenderer.Output textOutput;
- private MetadataRenderer.Output metadataOutput;
- private VideoListener videoListener;
private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener;
private DecoderCounters videoDecoderCounters;
@@ -113,6 +114,9 @@ public class SimpleExoPlayer implements ExoPlayer {
protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
LoadControl loadControl) {
componentListener = new ComponentListener();
+ videoListeners = new CopyOnWriteArraySet<>();
+ textOutputs = new CopyOnWriteArraySet<>();
+ metadataOutputs = new CopyOnWriteArraySet<>();
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
Handler eventHandler = new Handler(eventLooper);
renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
@@ -141,7 +145,7 @@ public class SimpleExoPlayer implements ExoPlayer {
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects.
- player = new ExoPlayerImpl(renderers, trackSelector, loadControl);
+ player = createExoPlayerImpl(renderers, trackSelector, loadControl);
}
/**
@@ -440,63 +444,132 @@ public class SimpleExoPlayer implements ExoPlayer {
}
/**
- * Sets a listener to receive video events.
+ * Adds a listener to receive video events.
+ *
+ * @param listener The listener to register.
+ */
+ public void addVideoListener(VideoListener listener) {
+ videoListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener of video events.
+ *
+ * @param listener The listener to unregister.
+ */
+ public void removeVideoListener(VideoListener listener) {
+ videoListeners.remove(listener);
+ }
+
+ /**
+ * Sets a listener to receive video events, removing all existing listeners.
*
* @param listener The listener.
+ * @deprecated Use {@link #addVideoListener(VideoListener)}.
*/
+ @Deprecated
public void setVideoListener(VideoListener listener) {
- videoListener = listener;
+ videoListeners.clear();
+ if (listener != null) {
+ addVideoListener(listener);
+ }
}
/**
- * Clears the listener receiving video events if it matches the one passed. Else does nothing.
+ * Equivalent to {@link #removeVideoListener(VideoListener)}.
*
* @param listener The listener to clear.
+ * @deprecated Use {@link #removeVideoListener(VideoListener)}.
*/
+ @Deprecated
public void clearVideoListener(VideoListener listener) {
- if (videoListener == listener) {
- videoListener = null;
- }
+ removeVideoListener(listener);
}
/**
- * Sets an output to receive text events.
+ * Registers an output to receive text events.
+ *
+ * @param listener The output to register.
+ */
+ public void addTextOutput(TextOutput listener) {
+ textOutputs.add(listener);
+ }
+
+ /**
+ * Removes a text output.
+ *
+ * @param listener The output to remove.
+ */
+ public void removeTextOutput(TextOutput listener) {
+ textOutputs.remove(listener);
+ }
+
+ /**
+ * Sets an output to receive text events, removing all existing outputs.
*
* @param output The output.
+ * @deprecated Use {@link #addTextOutput(TextOutput)}.
*/
- public void setTextOutput(TextRenderer.Output output) {
- textOutput = output;
- }
-
- /**
- * Clears the output receiving text events if it matches the one passed. Else does nothing.
- *
- * @param output The output to clear.
- */
- public void clearTextOutput(TextRenderer.Output output) {
- if (textOutput == output) {
- textOutput = null;
+ @Deprecated
+ public void setTextOutput(TextOutput output) {
+ textOutputs.clear();
+ if (output != null) {
+ addTextOutput(output);
}
}
/**
- * Sets a listener to receive metadata events.
+ * Equivalent to {@link #removeTextOutput(TextOutput)}.
+ *
+ * @param output The output to clear.
+ * @deprecated Use {@link #removeTextOutput(TextOutput)}.
+ */
+ @Deprecated
+ public void clearTextOutput(TextOutput output) {
+ removeTextOutput(output);
+ }
+
+ /**
+ * Registers an output to receive metadata events.
+ *
+ * @param listener The output to register.
+ */
+ public void addMetadataOutput(MetadataOutput listener) {
+ metadataOutputs.add(listener);
+ }
+
+ /**
+ * Removes a metadata output.
+ *
+ * @param listener The output to remove.
+ */
+ public void removeMetadataOutput(MetadataOutput listener) {
+ metadataOutputs.remove(listener);
+ }
+
+ /**
+ * Sets an output to receive metadata events, removing all existing outputs.
*
* @param output The output.
+ * @deprecated Use {@link #addMetadataOutput(MetadataOutput)}.
*/
- public void setMetadataOutput(MetadataRenderer.Output output) {
- metadataOutput = output;
+ @Deprecated
+ public void setMetadataOutput(MetadataOutput output) {
+ metadataOutputs.clear();
+ if (output != null) {
+ addMetadataOutput(output);
+ }
}
/**
- * Clears the output receiving metadata events if it matches the one passed. Else does nothing.
+ * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}.
*
* @param output The output to clear.
+ * @deprecated Use {@link #removeMetadataOutput(MetadataOutput)}.
*/
- public void clearMetadataOutput(MetadataRenderer.Output output) {
- if (metadataOutput == output) {
- metadataOutput = null;
- }
+ @Deprecated
+ public void clearMetadataOutput(MetadataOutput output) {
+ removeMetadataOutput(output);
}
/**
@@ -569,6 +642,16 @@ public class SimpleExoPlayer implements ExoPlayer {
player.setRepeatMode(repeatMode);
}
+ @Override
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ player.setShuffleModeEnabled(shuffleModeEnabled);
+ }
+
+ @Override
+ public boolean getShuffleModeEnabled() {
+ return player.getShuffleModeEnabled();
+ }
+
@Override
public boolean isLoading() {
return player.isLoading();
@@ -723,6 +806,19 @@ public class SimpleExoPlayer implements ExoPlayer {
// Internal methods.
+ /**
+ * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}.
+ *
+ * @param renderers The {@link Renderer}s that will be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @return A new {@link ExoPlayer} instance.
+ */
+ protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector,
+ LoadControl loadControl) {
+ return new ExoPlayerImpl(renderers, trackSelector, loadControl);
+ }
+
private void removeSurfaceCallbacks() {
if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) {
@@ -763,8 +859,8 @@ public class SimpleExoPlayer implements ExoPlayer {
}
private final class ComponentListener implements VideoRendererEventListener,
- AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
- SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
+ AudioRendererEventListener, TextOutput, MetadataOutput, SurfaceHolder.Callback,
+ TextureView.SurfaceTextureListener {
// VideoRendererEventListener implementation
@@ -803,7 +899,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
- if (videoListener != null) {
+ for (VideoListener videoListener : videoListeners) {
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
pixelWidthHeightRatio);
}
@@ -815,8 +911,10 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onRenderedFirstFrame(Surface surface) {
- if (videoListener != null && SimpleExoPlayer.this.surface == surface) {
- videoListener.onRenderedFirstFrame();
+ if (SimpleExoPlayer.this.surface == surface) {
+ for (VideoListener videoListener : videoListeners) {
+ videoListener.onRenderedFirstFrame();
+ }
}
if (videoDebugListener != null) {
videoDebugListener.onRenderedFirstFrame(surface);
@@ -885,20 +983,20 @@ public class SimpleExoPlayer implements ExoPlayer {
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
- // TextRenderer.Output implementation
+ // TextOutput implementation
@Override
public void onCues(List cues) {
- if (textOutput != null) {
+ for (TextOutput textOutput : textOutputs) {
textOutput.onCues(cues);
}
}
- // MetadataRenderer.Output implementation
+ // MetadataOutput implementation
@Override
public void onMetadata(Metadata metadata) {
- if (metadataOutput != null) {
+ for (MetadataOutput metadataOutput : metadataOutputs) {
metadataOutput.onMetadata(metadata);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
index 414c0804ad..7d4c1995eb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -593,30 +593,6 @@ public abstract class Timeline {
}
}
- /**
- * Returns whether the given window is the last window of the timeline depending on the
- * {@code repeatMode}.
- *
- * @param windowIndex A window index.
- * @param repeatMode A repeat mode.
- * @return Whether the window of the given index is the last window of the timeline.
- */
- public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) {
- return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET;
- }
-
- /**
- * Returns whether the given window is the first window of the timeline depending on the
- * {@code repeatMode}.
- *
- * @param windowIndex A window index.
- * @param repeatMode A repeat mode.
- * @return Whether the window of the given index is the first window of the timeline.
- */
- public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) {
- return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET;
- }
-
/**
* Populates a {@link Window} with data for the window at the specified index. Does not populate
* {@link Window#id}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
index 4b64ffb030..e1a70e2579 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -33,11 +33,33 @@ public final class Ac3Util {
*/
public static final class Ac3SyncFrameInfo {
+ /**
+ * Undefined AC3 stream type.
+ */
+ public static final int STREAM_TYPE_UNDEFINED = -1;
+ /**
+ * Type 0 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1.
+ */
+ public static final int STREAM_TYPE_TYPE0 = 0;
+ /**
+ * Type 1 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1.
+ */
+ public static final int STREAM_TYPE_TYPE1 = 1;
+ /**
+ * Type 2 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1.
+ */
+ public static final int STREAM_TYPE_TYPE2 = 2;
+
/**
* The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and
* {@link MimeTypes#AUDIO_E_AC3}.
*/
public final String mimeType;
+ /**
+ * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or
+ * {@link #STREAM_TYPE_UNDEFINED} otherwise.
+ */
+ public final int streamType;
/**
* The audio sampling rate in Hz.
*/
@@ -55,9 +77,10 @@ public final class Ac3Util {
*/
public final int sampleCount;
- private Ac3SyncFrameInfo(String mimeType, int channelCount, int sampleRate, int frameSize,
- int sampleCount) {
+ private Ac3SyncFrameInfo(String mimeType, int streamType, int channelCount, int sampleRate,
+ int frameSize, int sampleCount) {
this.mimeType = mimeType;
+ this.streamType = streamType;
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.frameSize = frameSize;
@@ -138,8 +161,7 @@ public final class Ac3Util {
String language, DrmInitData drmInitData) {
data.skipBytes(2); // data_rate, num_ind_sub
- // Read only the first substream.
- // TODO: Read later substreams?
+ // Read the first independent substream.
int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
int nextByte = data.readUnsignedByte();
@@ -147,6 +169,18 @@ public final class Ac3Util {
if ((nextByte & 0x01) != 0) { // lfeon
channelCount++;
}
+
+ // Read the first dependent substream.
+ nextByte = data.readUnsignedByte();
+ int numDepSub = ((nextByte & 0x1E) >> 1);
+ if (numDepSub > 0) {
+ int lowByteChanLoc = data.readUnsignedByte();
+ // Read Lrs/Rrs pair
+ // TODO: Read other channel configuration
+ if ((lowByteChanLoc & 0x02) != 0) {
+ channelCount += 2;
+ }
+ }
return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE,
Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
}
@@ -164,13 +198,16 @@ public final class Ac3Util {
boolean isEac3 = data.readBits(5) == 16;
data.setPosition(initialPosition);
String mimeType;
+ int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED;
int sampleRate;
int acmod;
int frameSize;
int sampleCount;
if (isEac3) {
mimeType = MimeTypes.AUDIO_E_AC3;
- data.skipBits(16 + 2 + 3); // syncword, strmtype, substreamid
+ data.skipBits(16); // syncword
+ streamType = data.readBits(2);
+ data.skipBits(3); // substreamid
frameSize = (data.readBits(11) + 1) * 2;
int fscod = data.readBits(2);
int audioBlocks;
@@ -206,7 +243,8 @@ public final class Ac3Util {
}
boolean lfeon = data.readBit();
int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
- return new Ac3SyncFrameInfo(mimeType, channelCount, sampleRate, frameSize, sampleCount);
+ return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize,
+ sampleCount);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
index 612018917b..5f9f599f01 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio;
import android.os.Handler;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Renderer;
@@ -84,15 +85,16 @@ public interface AudioRendererEventListener {
*/
final class EventDispatcher {
- private final Handler handler;
- private final AudioRendererEventListener listener;
+ @Nullable private final Handler handler;
+ @Nullable private final AudioRendererEventListener listener;
/**
* @param handler A handler for dispatching events, or null if creating a dummy instance.
* @param listener The listener to which events should be dispatched, or null if creating a
* dummy instance.
*/
- public EventDispatcher(Handler handler, AudioRendererEventListener listener) {
+ public EventDispatcher(@Nullable Handler handler,
+ @Nullable AudioRendererEventListener listener) {
this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
this.listener = listener;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
index 79cb26bf39..d7ebd69fbf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -22,6 +22,7 @@ import android.media.AudioManager;
import android.media.AudioTimestamp;
import android.os.ConditionVariable;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
@@ -277,7 +278,7 @@ public final class AudioTrack {
*/
public static boolean failOnSpuriousAudioTimestamp = false;
- private final AudioCapabilities audioCapabilities;
+ @Nullable private final AudioCapabilities audioCapabilities;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
private final SonicAudioProcessor sonicAudioProcessor;
private final AudioProcessor[] availableAudioProcessors;
@@ -355,7 +356,7 @@ public final class AudioTrack {
* output. May be empty.
* @param listener Listener for audio track events.
*/
- public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors,
+ public AudioTrack(@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors,
Listener listener) {
this.audioCapabilities = audioCapabilities;
this.listener = listener;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index 4d97c292ac..e146238dcc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -21,6 +21,7 @@ import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
@@ -72,7 +73,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys) {
this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
}
@@ -83,8 +84,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
- public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler,
- AudioRendererEventListener eventListener) {
+ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) {
this(mediaCodecSelector, null, true, eventHandler, eventListener);
}
@@ -102,9 +103,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
- DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, Handler eventHandler,
- AudioRendererEventListener eventListener) {
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener) {
this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener, null);
}
@@ -127,10 +128,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* output.
*/
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
- DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, Handler eventHandler,
- AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
- AudioProcessor... audioProcessors) {
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java
index 3c2d6d96e9..7a532110d3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java
@@ -36,6 +36,12 @@ public final class DecoderCounters {
* The number of queued input buffers.
*/
public int inputBufferCount;
+ /**
+ * The number of skipped input buffers.
+ *
+ * A skipped input buffer is an input buffer that was deliberately not sent to the decoder.
+ */
+ public int skippedInputBufferCount;
/**
* The number of rendered output buffers.
*/
@@ -79,6 +85,7 @@ public final class DecoderCounters {
decoderInitCount += other.decoderInitCount;
decoderReleaseCount += other.decoderReleaseCount;
inputBufferCount += other.inputBufferCount;
+ skippedInputBufferCount += other.skippedInputBufferCount;
renderedOutputBufferCount += other.renderedOutputBufferCount;
skippedOutputBufferCount += other.skippedOutputBufferCount;
droppedOutputBufferCount += other.droppedOutputBufferCount;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java
new file mode 100644
index 0000000000..cfb2cf9d8a
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link DrmSession} that supports playbacks using {@link MediaDrm}.
+ */
+@TargetApi(18)
+/* package */ class DefaultDrmSession implements DrmSession {
+ private static final String TAG = "DefaultDrmSession";
+
+ private static final String CENC_SCHEME_MIME_TYPE = "cenc";
+
+ private static final int MSG_PROVISION = 0;
+ private static final int MSG_KEYS = 1;
+
+ private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
+
+ private final Handler eventHandler;
+ private final DefaultDrmSessionManager.EventListener eventListener;
+ private final ExoMediaDrm mediaDrm;
+ private final HashMap optionalKeyRequestParameters;
+ /* package */ final MediaDrmCallback callback;
+ /* package */ final UUID uuid;
+ /* package */ MediaDrmHandler mediaDrmHandler;
+ /* package */ PostResponseHandler postResponseHandler;
+ private HandlerThread requestHandlerThread;
+ private Handler postRequestHandler;
+
+ @DefaultDrmSessionManager.Mode
+ private final int mode;
+ private int openCount;
+ private boolean provisioningInProgress;
+ @DrmSession.State
+ private int state;
+ private T mediaCrypto;
+ private DrmSessionException lastException;
+ private final byte[] schemeInitData;
+ private final String schemeMimeType;
+ private byte[] sessionId;
+ private byte[] offlineLicenseKeySetId;
+
+ /**
+ * Instantiates a new DRM session.
+ *
+ * @param uuid The UUID of the drm scheme.
+ * @param mediaDrm The media DRM.
+ * @param initData The DRM init data.
+ * @param mode The DRM mode.
+ * @param offlineLicenseKeySetId The offlineLicense KeySetId.
+ * @param optionalKeyRequestParameters The optional key request parameters.
+ * @param callback The media DRM callback.
+ * @param playbackLooper The playback looper.
+ * @param eventHandler The handler to post listener events.
+ * @param eventListener The DRM session manager event listener.
+ */
+ public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, DrmInitData initData,
+ @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId,
+ HashMap optionalKeyRequestParameters, MediaDrmCallback callback,
+ Looper playbackLooper, Handler eventHandler,
+ DefaultDrmSessionManager.EventListener eventListener) {
+ this.uuid = uuid;
+ this.mediaDrm = mediaDrm;
+ this.mode = mode;
+ this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+ this.optionalKeyRequestParameters = optionalKeyRequestParameters;
+ this.callback = callback;
+
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ state = STATE_OPENING;
+
+ mediaDrmHandler = new MediaDrmHandler(playbackLooper);
+ mediaDrm.setOnEventListener(new MediaDrmEventListener());
+ postResponseHandler = new PostResponseHandler(playbackLooper);
+ requestHandlerThread = new HandlerThread("DrmRequestHandler");
+ requestHandlerThread.start();
+ postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
+
+ // Parse init data.
+ byte[] schemeInitData = null;
+ String schemeMimeType = null;
+ if (offlineLicenseKeySetId == null) {
+ SchemeData data = getSchemeData(initData, uuid);
+ if (data == null) {
+ onError(new IllegalStateException("Media does not support uuid: " + uuid));
+ } else {
+ schemeInitData = data.data;
+ schemeMimeType = data.mimeType;
+ if (Util.SDK_INT < 21) {
+ // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
+ byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid);
+ if (psshData == null) {
+ // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
+ } else {
+ schemeInitData = psshData;
+ }
+ }
+ if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
+ && (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
+ || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
+ // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
+ schemeMimeType = CENC_SCHEME_MIME_TYPE;
+ }
+ }
+ }
+ this.schemeInitData = schemeInitData;
+ this.schemeMimeType = schemeMimeType;
+ }
+
+ // Life cycle.
+
+ public void acquire() {
+ if (++openCount == 1) {
+ if (state == STATE_ERROR) {
+ return;
+ }
+ if (openInternal(true)) {
+ doLicense();
+ }
+ }
+ }
+
+ /**
+ * @return True if the session is closed and cleaned up, false otherwise.
+ */
+ public boolean release() {
+ if (--openCount == 0) {
+ state = STATE_RELEASED;
+ provisioningInProgress = false;
+ mediaDrmHandler.removeCallbacksAndMessages(null);
+ mediaDrmHandler = null;
+ postResponseHandler.removeCallbacksAndMessages(null);
+ postRequestHandler.removeCallbacksAndMessages(null);
+ postRequestHandler = null;
+ requestHandlerThread.quit();
+ requestHandlerThread = null;
+ mediaCrypto = null;
+ lastException = null;
+ if (sessionId != null) {
+ mediaDrm.closeSession(sessionId);
+ sessionId = null;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // DrmSession Implementation.
+
+ @Override
+ @DrmSession.State
+ public final int getState() {
+ return state;
+ }
+
+ @Override
+ public final DrmSessionException getError() {
+ return state == STATE_ERROR ? lastException : null;
+ }
+
+ @Override
+ public final T getMediaCrypto() {
+ return mediaCrypto;
+ }
+
+ @Override
+ public Map queryKeyStatus() {
+ return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ public byte[] getOfflineLicenseKeySetId() {
+ return offlineLicenseKeySetId;
+ }
+
+ // Internal methods.
+
+ /**
+ * Try to open a session, do provisioning if necessary.
+ * @param allowProvisioning if provisioning is allowed, set this to false when calling from
+ * processing provision response.
+ * @return true on success, false otherwise.
+ */
+ private boolean openInternal(boolean allowProvisioning) {
+ if (isOpen()) {
+ // Already opened
+ return true;
+ }
+
+ try {
+ sessionId = mediaDrm.openSession();
+ mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
+ state = STATE_OPENED;
+ return true;
+ } catch (NotProvisionedException e) {
+ if (allowProvisioning) {
+ postProvisionRequest();
+ } else {
+ onError(e);
+ }
+ } catch (Exception e) {
+ // MediaCryptoException
+ // ResourceBusyException only available on 19+
+ onError(e);
+ }
+
+ return false;
+ }
+
+ private void postProvisionRequest() {
+ if (provisioningInProgress) {
+ return;
+ }
+ provisioningInProgress = true;
+ ProvisionRequest request = mediaDrm.getProvisionRequest();
+ postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
+ }
+
+ private void onProvisionResponse(Object response) {
+ provisioningInProgress = false;
+ if (state != STATE_OPENING && !isOpen()) {
+ // This event is stale.
+ return;
+ }
+
+ if (response instanceof Exception) {
+ onError((Exception) response);
+ return;
+ }
+
+ try {
+ mediaDrm.provideProvisionResponse((byte[]) response);
+ if (openInternal(false)) {
+ doLicense();
+ }
+ } catch (DeniedByServerException e) {
+ onError(e);
+ }
+ }
+
+ private void doLicense() {
+ switch (mode) {
+ case DefaultDrmSessionManager.MODE_PLAYBACK:
+ case DefaultDrmSessionManager.MODE_QUERY:
+ if (offlineLicenseKeySetId == null) {
+ postKeyRequest(MediaDrm.KEY_TYPE_STREAMING);
+ } else {
+ if (restoreKeys()) {
+ long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
+ if (mode == DefaultDrmSessionManager.MODE_PLAYBACK
+ && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) {
+ Log.d(TAG, "Offline license has expired or will expire soon. "
+ + "Remaining seconds: " + licenseDurationRemainingSec);
+ postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE);
+ } else if (licenseDurationRemainingSec <= 0) {
+ onError(new KeysExpiredException());
+ } else {
+ state = STATE_OPENED_WITH_KEYS;
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmKeysRestored();
+ }
+ });
+ }
+ }
+ }
+ }
+ break;
+ case DefaultDrmSessionManager.MODE_DOWNLOAD:
+ if (offlineLicenseKeySetId == null) {
+ postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE);
+ } else {
+ // Renew
+ if (restoreKeys()) {
+ postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE);
+ }
+ }
+ break;
+ case DefaultDrmSessionManager.MODE_RELEASE:
+ // It's not necessary to restore the key (and open a session to do that) before releasing it
+ // but this serves as a good sanity/fast-failure check.
+ if (restoreKeys()) {
+ postKeyRequest(MediaDrm.KEY_TYPE_RELEASE);
+ }
+ break;
+ }
+ }
+
+ private boolean restoreKeys() {
+ try {
+ mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to restore Widevine keys.", e);
+ onError(e);
+ }
+ return false;
+ }
+
+ private long getLicenseDurationRemainingSec() {
+ if (!C.WIDEVINE_UUID.equals(uuid)) {
+ return Long.MAX_VALUE;
+ }
+ Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this);
+ return Math.min(pair.first, pair.second);
+ }
+
+ private void postKeyRequest(int type) {
+ byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId;
+ try {
+ KeyRequest request = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, type,
+ optionalKeyRequestParameters);
+ postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget();
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeyResponse(Object response) {
+ if (!isOpen()) {
+ // This event is stale.
+ return;
+ }
+
+ if (response instanceof Exception) {
+ onKeysError((Exception) response);
+ return;
+ }
+
+ try {
+ if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
+ mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmKeysRemoved();
+ }
+ });
+ }
+ } else {
+ byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
+ if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
+ || (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null))
+ && keySetId != null && keySetId.length != 0) {
+ offlineLicenseKeySetId = keySetId;
+ }
+ state = STATE_OPENED_WITH_KEYS;
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmKeysLoaded();
+ }
+ });
+ }
+ }
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeysExpired() {
+ if (state == STATE_OPENED_WITH_KEYS) {
+ state = STATE_OPENED;
+ onError(new KeysExpiredException());
+ }
+ }
+
+ private void onKeysError(Exception e) {
+ if (e instanceof NotProvisionedException) {
+ postProvisionRequest();
+ } else {
+ onError(e);
+ }
+ }
+
+ private void onError(final Exception e) {
+ lastException = new DrmSessionException(e);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmSessionManagerError(e);
+ }
+ });
+ }
+ if (state != STATE_OPENED_WITH_KEYS) {
+ state = STATE_ERROR;
+ }
+ }
+
+ private boolean isOpen() {
+ return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class MediaDrmHandler extends Handler {
+
+ public MediaDrmHandler(Looper looper) {
+ super(looper);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void handleMessage(Message msg) {
+ if (!isOpen()) {
+ return;
+ }
+ switch (msg.what) {
+ case MediaDrm.EVENT_KEY_REQUIRED:
+ doLicense();
+ break;
+ case MediaDrm.EVENT_KEY_EXPIRED:
+ // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
+ // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
+ // waiting for key response.
+ onKeysExpired();
+ break;
+ case MediaDrm.EVENT_PROVISION_REQUIRED:
+ state = STATE_OPENED;
+ postProvisionRequest();
+ break;
+ }
+ }
+
+ }
+
+ private class MediaDrmEventListener implements OnEventListener {
+
+ @Override
+ public void onEvent(ExoMediaDrm extends T> md, byte[] sessionId, int event, int extra,
+ byte[] data) {
+ if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) {
+ mediaDrmHandler.sendEmptyMessage(event);
+ }
+ }
+
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class PostResponseHandler extends Handler {
+
+ public PostResponseHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ onProvisionResponse(msg.obj);
+ break;
+ case MSG_KEYS:
+ onKeyResponse(msg.obj);
+ break;
+ }
+ }
+
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class PostRequestHandler extends Handler {
+
+ public PostRequestHandler(Looper backgroundLooper) {
+ super(backgroundLooper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Object response;
+ try {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
+ break;
+ case MSG_KEYS:
+ response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ } catch (Exception e) {
+ response = e;
+ }
+ postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
+ }
+
+ }
+
+ /**
+ * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}.
+ *
+ * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.
+ * @param uuid The UUID of the scheme.
+ * @return The extracted {@link SchemeData}, or null if no suitable data is present.
+ */
+ public static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) {
+ SchemeData schemeData = drmInitData.get(uuid);
+ if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) {
+ // If present, the Common PSSH box should be used for ClearKey.
+ schemeData = drmInitData.get(C.COMMON_PSSH_UUID);
+ }
+ return schemeData;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index cafbe6e8f7..25a73a67c1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -15,41 +15,27 @@
*/
package com.google.android.exoplayer2.drm;
-import android.annotation.SuppressLint;
import android.annotation.TargetApi;
-import android.media.DeniedByServerException;
import android.media.MediaDrm;
-import android.media.NotProvisionedException;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
-import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
-import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
-import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
-import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
-import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
-import java.util.Map;
import java.util.UUID;
/**
* A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
*/
@TargetApi(18)
-public class DefaultDrmSessionManager implements DrmSessionManager,
- DrmSession {
+public class DefaultDrmSessionManager implements DrmSessionManager {
/**
* Listener of {@link DefaultDrmSessionManager} events.
@@ -95,8 +81,7 @@ public class DefaultDrmSessionManager implements DrmSe
*/
public static final int MODE_PLAYBACK = 0;
/**
- * Restores an offline license to allow its status to be queried. If the offline license is
- * expired sets state to {@link #STATE_ERROR}.
+ * Restores an offline license to allow its status to be queried.
*/
public static final int MODE_QUERY = 1;
/** Downloads an offline license or renews an existing one. */
@@ -104,40 +89,18 @@ public class DefaultDrmSessionManager implements DrmSe
/** Releases an existing offline license. */
public static final int MODE_RELEASE = 3;
- private static final String TAG = "OfflineDrmSessionMngr";
- private static final String CENC_SCHEME_MIME_TYPE = "cenc";
-
- private static final int MSG_PROVISION = 0;
- private static final int MSG_KEYS = 1;
-
- private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
-
private final Handler eventHandler;
private final EventListener eventListener;
private final ExoMediaDrm mediaDrm;
private final HashMap optionalKeyRequestParameters;
- /* package */ final MediaDrmCallback callback;
- /* package */ final UUID uuid;
-
- /* package */ MediaDrmHandler mediaDrmHandler;
- /* package */ PostResponseHandler postResponseHandler;
+ private final MediaDrmCallback callback;
+ private final UUID uuid;
private Looper playbackLooper;
- private HandlerThread requestHandlerThread;
- private Handler postRequestHandler;
-
private int mode;
- private int openCount;
- private boolean provisioningInProgress;
- @DrmSession.State
- private int state;
- private T mediaCrypto;
- private DrmSessionException lastException;
- private byte[] schemeInitData;
- private String schemeMimeType;
- private byte[] sessionId;
private byte[] offlineLicenseKeySetId;
+ private DefaultDrmSession session;
/**
* Instantiates a new instance using the Widevine scheme.
@@ -216,13 +179,14 @@ public class DefaultDrmSessionManager implements DrmSe
public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
HashMap optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener) {
+ Assertions.checkNotNull(uuid);
+ Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid;
this.mediaDrm = mediaDrm;
this.callback = callback;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
- mediaDrm.setOnEventListener(new MediaDrmEventListener());
mode = MODE_PLAYBACK;
}
@@ -297,7 +261,7 @@ public class DefaultDrmSessionManager implements DrmSe
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
*/
public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) {
- Assertions.checkState(openCount == 0);
+ Assertions.checkState(session == null);
if (mode == MODE_QUERY || mode == MODE_RELEASE) {
Assertions.checkNotNull(offlineLicenseKeySetId);
}
@@ -309,7 +273,7 @@ public class DefaultDrmSessionManager implements DrmSe
@Override
public boolean canAcquireSession(@NonNull DrmInitData drmInitData) {
- SchemeData schemeData = drmInitData.get(uuid);
+ SchemeData schemeData = DefaultDrmSession.getSchemeData(drmInitData, uuid);
if (schemeData == null) {
// No data for this manager's scheme.
return false;
@@ -330,392 +294,22 @@ public class DefaultDrmSessionManager implements DrmSe
@Override
public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
- if (++openCount != 1) {
- return this;
- }
-
- if (this.playbackLooper == null) {
+ if (session == null) {
this.playbackLooper = playbackLooper;
- mediaDrmHandler = new MediaDrmHandler(playbackLooper);
- postResponseHandler = new PostResponseHandler(playbackLooper);
+ session = new DefaultDrmSession(uuid, mediaDrm, drmInitData, mode, offlineLicenseKeySetId,
+ optionalKeyRequestParameters, callback, playbackLooper, eventHandler, eventListener);
}
- requestHandlerThread = new HandlerThread("DrmRequestHandler");
- requestHandlerThread.start();
- postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
-
- if (offlineLicenseKeySetId == null) {
- SchemeData schemeData = drmInitData.get(uuid);
- if (schemeData == null) {
- onError(new IllegalStateException("Media does not support uuid: " + uuid));
- return this;
- }
- schemeInitData = schemeData.data;
- schemeMimeType = schemeData.mimeType;
- if (Util.SDK_INT < 21) {
- // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
- byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
- if (psshData == null) {
- // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
- } else {
- schemeInitData = psshData;
- }
- }
- if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
- && (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
- || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
- // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
- schemeMimeType = CENC_SCHEME_MIME_TYPE;
- }
- }
- state = STATE_OPENING;
- openInternal(true);
- return this;
+ session.acquire();
+ return session;
}
@Override
public void releaseSession(DrmSession session) {
- if (--openCount != 0) {
- return;
+ Assertions.checkState(session == this.session);
+ if (this.session.release()) {
+ this.session = null;
}
- state = STATE_RELEASED;
- provisioningInProgress = false;
- mediaDrmHandler.removeCallbacksAndMessages(null);
- postResponseHandler.removeCallbacksAndMessages(null);
- postRequestHandler.removeCallbacksAndMessages(null);
- postRequestHandler = null;
- requestHandlerThread.quit();
- requestHandlerThread = null;
- schemeInitData = null;
- schemeMimeType = null;
- mediaCrypto = null;
- lastException = null;
- if (sessionId != null) {
- mediaDrm.closeSession(sessionId);
- sessionId = null;
- }
- }
-
- // DrmSession implementation.
-
- @Override
- @DrmSession.State
- public final int getState() {
- return state;
- }
-
- @Override
- public final DrmSessionException getError() {
- return state == STATE_ERROR ? lastException : null;
- }
-
- @Override
- public final T getMediaCrypto() {
- return mediaCrypto;
- }
-
- @Override
- public Map queryKeyStatus() {
- return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);
- }
-
- @Override
- public byte[] getOfflineLicenseKeySetId() {
- return offlineLicenseKeySetId;
- }
-
- // Internal methods.
-
- private void openInternal(boolean allowProvisioning) {
- try {
- sessionId = mediaDrm.openSession();
- mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
- state = STATE_OPENED;
- doLicense();
- } catch (NotProvisionedException e) {
- if (allowProvisioning) {
- postProvisionRequest();
- } else {
- onError(e);
- }
- } catch (Exception e) {
- onError(e);
- }
- }
-
- private void postProvisionRequest() {
- if (provisioningInProgress) {
- return;
- }
- provisioningInProgress = true;
- ProvisionRequest request = mediaDrm.getProvisionRequest();
- postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
- }
-
- private void onProvisionResponse(Object response) {
- provisioningInProgress = false;
- if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
- // This event is stale.
- return;
- }
-
- if (response instanceof Exception) {
- onError((Exception) response);
- return;
- }
-
- try {
- mediaDrm.provideProvisionResponse((byte[]) response);
- if (state == STATE_OPENING) {
- openInternal(false);
- } else {
- doLicense();
- }
- } catch (DeniedByServerException e) {
- onError(e);
- }
- }
-
- private void doLicense() {
- switch (mode) {
- case MODE_PLAYBACK:
- case MODE_QUERY:
- if (offlineLicenseKeySetId == null) {
- postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING);
- } else {
- if (restoreKeys()) {
- long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
- if (mode == MODE_PLAYBACK
- && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) {
- Log.d(TAG, "Offline license has expired or will expire soon. "
- + "Remaining seconds: " + licenseDurationRemainingSec);
- postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
- } else if (licenseDurationRemainingSec <= 0) {
- onError(new KeysExpiredException());
- } else {
- state = STATE_OPENED_WITH_KEYS;
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysRestored();
- }
- });
- }
- }
- }
- }
- break;
- case MODE_DOWNLOAD:
- if (offlineLicenseKeySetId == null) {
- postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
- } else {
- // Renew
- if (restoreKeys()) {
- postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
- }
- }
- break;
- case MODE_RELEASE:
- // It's not necessary to restore the key (and open a session to do that) before releasing it
- // but this serves as a good sanity/fast-failure check.
- if (restoreKeys()) {
- postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
- }
- break;
- }
- }
-
- private boolean restoreKeys() {
- try {
- mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
- return true;
- } catch (Exception e) {
- Log.e(TAG, "Error trying to restore Widevine keys.", e);
- onError(e);
- }
- return false;
- }
-
- private long getLicenseDurationRemainingSec() {
- if (!C.WIDEVINE_UUID.equals(uuid)) {
- return Long.MAX_VALUE;
- }
- Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this);
- return Math.min(pair.first, pair.second);
- }
-
- private void postKeyRequest(byte[] scope, int keyType) {
- try {
- KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
- optionalKeyRequestParameters);
- postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
- } catch (Exception e) {
- onKeysError(e);
- }
- }
-
- private void onKeyResponse(Object response) {
- if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
- // This event is stale.
- return;
- }
-
- if (response instanceof Exception) {
- onKeysError((Exception) response);
- return;
- }
-
- try {
- if (mode == MODE_RELEASE) {
- mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysRemoved();
- }
- });
- }
- } else {
- byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
- if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null))
- && keySetId != null && keySetId.length != 0) {
- offlineLicenseKeySetId = keySetId;
- }
- state = STATE_OPENED_WITH_KEYS;
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysLoaded();
- }
- });
- }
- }
- } catch (Exception e) {
- onKeysError(e);
- }
- }
-
- private void onKeysError(Exception e) {
- if (e instanceof NotProvisionedException) {
- postProvisionRequest();
- } else {
- onError(e);
- }
- }
-
- private void onError(final Exception e) {
- lastException = new DrmSessionException(e);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmSessionManagerError(e);
- }
- });
- }
- if (state != STATE_OPENED_WITH_KEYS) {
- state = STATE_ERROR;
- }
- }
-
- @SuppressLint("HandlerLeak")
- private class MediaDrmHandler extends Handler {
-
- public MediaDrmHandler(Looper looper) {
- super(looper);
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void handleMessage(Message msg) {
- if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
- return;
- }
- switch (msg.what) {
- case MediaDrm.EVENT_KEY_REQUIRED:
- doLicense();
- break;
- case MediaDrm.EVENT_KEY_EXPIRED:
- // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
- // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
- // waiting for key response.
- if (state == STATE_OPENED_WITH_KEYS) {
- state = STATE_OPENED;
- onError(new KeysExpiredException());
- }
- break;
- case MediaDrm.EVENT_PROVISION_REQUIRED:
- state = STATE_OPENED;
- postProvisionRequest();
- break;
- }
- }
-
- }
-
- private class MediaDrmEventListener implements OnEventListener {
-
- @Override
- public void onEvent(ExoMediaDrm extends T> md, byte[] sessionId, int event, int extra,
- byte[] data) {
- if (mode == MODE_PLAYBACK) {
- mediaDrmHandler.sendEmptyMessage(event);
- }
- }
-
- }
-
- @SuppressLint("HandlerLeak")
- private class PostResponseHandler extends Handler {
-
- public PostResponseHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_PROVISION:
- onProvisionResponse(msg.obj);
- break;
- case MSG_KEYS:
- onKeyResponse(msg.obj);
- break;
- }
- }
-
- }
-
- @SuppressLint("HandlerLeak")
- private class PostRequestHandler extends Handler {
-
- public PostRequestHandler(Looper backgroundLooper) {
- super(backgroundLooper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- Object response;
- try {
- switch (msg.what) {
- case MSG_PROVISION:
- response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
- break;
- case MSG_KEYS:
- response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
- break;
- default:
- throw new RuntimeException();
- }
- } catch (Exception e) {
- response = e;
- }
- postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
- }
-
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
index ed4494559a..5d0cb038d4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -57,7 +57,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm keyRequestProperties;
/**
- * @param defaultUrl The default license URL.
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
*/
- public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) {
- this(defaultUrl, dataSourceFactory, null);
+ public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
+ this(defaultLicenseUrl, false, dataSourceFactory);
}
/**
- * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request
- * properties can be set by calling {@link #setKeyRequestProperty(String, String)}.
- * @param defaultUrl The default license URL.
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is
+ * set to true.
+ * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
+ * include their own license URL.
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
- * @param keyRequestProperties Request properties to set when making key requests, or null.
*/
- @Deprecated
- public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
- Map keyRequestProperties) {
+ public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
+ HttpDataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
- this.defaultUrl = defaultUrl;
+ this.defaultLicenseUrl = defaultLicenseUrl;
+ this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
this.keyRequestProperties = new HashMap<>();
- if (keyRequestProperties != null) {
- this.keyRequestProperties.putAll(keyRequestProperties);
- }
}
/**
@@ -112,8 +111,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
@Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
String url = request.getDefaultUrl();
- if (TextUtils.isEmpty(url)) {
- url = defaultUrl;
+ if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
+ url = defaultLicenseUrl;
}
Map requestProperties = new HashMap<>();
// Add standard request properties for supported schemes.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
index 040ca50c76..cafe41ed09 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.google.android.exoplayer2.drm;
import android.media.MediaDrm;
@@ -44,23 +43,47 @@ public final class OfflineLicenseHelper {
* Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
* is no longer required.
*
- * @param licenseUrl The default license URL.
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
* @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
* @return A new instance which uses Widevine CDM.
* @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
* instantiated.
*/
public static OfflineLicenseHelper newWidevineInstance(
- String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
- return newWidevineInstance(
- new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null);
+ String defaultLicenseUrl, Factory httpDataSourceFactory)
+ throws UnsupportedDrmException {
+ return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null);
}
/**
* Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
* is no longer required.
*
- * @param callback Performs key and provisioning requests.
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
+ * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
+ * include their own license URL.
+ * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ * @return A new instance which uses Widevine CDM.
+ * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+ * instantiated.
+ */
+ public static OfflineLicenseHelper newWidevineInstance(
+ String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory)
+ throws UnsupportedDrmException {
+ return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory,
+ null);
+ }
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
+ * is no longer required.
+ *
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
+ * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
+ * include their own license URL.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
* @return A new instance which uses Widevine CDM.
@@ -70,9 +93,11 @@ public final class OfflineLicenseHelper {
* MediaDrmCallback, HashMap, Handler, EventListener)
*/
public static OfflineLicenseHelper newWidevineInstance(
- MediaDrmCallback callback, HashMap optionalKeyRequestParameters)
+ String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory,
+ HashMap optionalKeyRequestParameters)
throws UnsupportedDrmException {
- return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback,
+ return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID),
+ new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory),
optionalKeyRequestParameters);
}
@@ -116,9 +141,32 @@ public final class OfflineLicenseHelper {
optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener);
}
- /** Releases the helper. Should be called when the helper is no longer required. */
- public void release() {
- handlerThread.quit();
+ /**
+ * @see DefaultDrmSessionManager#getPropertyByteArray
+ */
+ public synchronized byte[] getPropertyByteArray(String key) {
+ return drmSessionManager.getPropertyByteArray(key);
+ }
+
+ /**
+ * @see DefaultDrmSessionManager#setPropertyByteArray
+ */
+ public synchronized void setPropertyByteArray(String key, byte[] value) {
+ drmSessionManager.setPropertyByteArray(key, value);
+ }
+
+ /**
+ * @see DefaultDrmSessionManager#getPropertyString
+ */
+ public synchronized String getPropertyString(String key) {
+ return drmSessionManager.getPropertyString(key);
+ }
+
+ /**
+ * @see DefaultDrmSessionManager#setPropertyString
+ */
+ public synchronized void setPropertyString(String key, String value) {
+ drmSessionManager.setPropertyString(key, value);
}
/**
@@ -186,6 +234,13 @@ public final class OfflineLicenseHelper {
return licenseDurationRemainingSec;
}
+ /**
+ * Releases the helper. Should be called when the helper is no longer required.
+ */
+ public void release() {
+ handlerThread.quit();
+ }
+
private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
DrmInitData drmInitData) throws DrmSessionException {
DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
index 2f21898007..ec5ad88aeb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.flv;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -85,7 +86,7 @@ import java.util.Collections;
}
@Override
- protected void parsePayload(ParsableByteArray data, long timeUs) {
+ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
if (audioFormat == AUDIO_FORMAT_MP3) {
int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
index cc7e662336..21d861af30 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -39,9 +39,14 @@ import java.util.List;
public static final int LONG_HEADER_SIZE = 16;
/**
- * Value for the first 32 bits of atomSize when the atom size is actually a long value.
+ * Value for the size field in an atom that defines its size in the largesize field.
*/
- public static final int LONG_SIZE_PREFIX = 1;
+ public static final int DEFINES_LARGE_SIZE = 1;
+
+ /**
+ * Value for the size field in an atom that extends to the end of the file.
+ */
+ public static final int EXTENDS_TO_END_SIZE = 0;
public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index f7e3e846e9..450e0682e6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -816,7 +816,7 @@ import java.util.List;
private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,
- StsdData out, int entryIndex) {
+ StsdData out, int entryIndex) throws ParserException {
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
int quickTimeSoundDescriptionVersion = 0;
@@ -995,9 +995,10 @@ import java.util.List;
int objectTypeIndication = parent.readUnsignedByte();
String mimeType;
switch (objectTypeIndication) {
- case 0x6B:
- mimeType = MimeTypes.AUDIO_MPEG;
- return Pair.create(mimeType, null);
+ case 0x60:
+ case 0x61:
+ mimeType = MimeTypes.VIDEO_MPEG2;
+ break;
case 0x20:
mimeType = MimeTypes.VIDEO_MP4V;
break;
@@ -1007,6 +1008,9 @@ import java.util.List;
case 0x23:
mimeType = MimeTypes.VIDEO_H265;
break;
+ case 0x6B:
+ mimeType = MimeTypes.AUDIO_MPEG;
+ return Pair.create(mimeType, null);
case 0x40:
case 0x66:
case 0x67:
@@ -1034,8 +1038,8 @@ import java.util.List;
parent.skipBytes(12);
- // Start of the AudioSpecificConfig.
- parent.skipBytes(1); // AudioSpecificConfig tag
+ // Start of the DecoderSpecificInfo.
+ parent.skipBytes(1); // DecoderSpecificInfo tag
int initializationDataSize = parseExpandableClassSize(parent);
byte[] initializationData = new byte[initializationDataSize];
parent.readBytes(initializationData, 0, initializationDataSize);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index 6b2077ef76..c3f2a9fb38 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -283,12 +283,22 @@ public final class FragmentedMp4Extractor implements Extractor {
atomType = atomHeader.readInt();
}
- if (atomSize == Atom.LONG_SIZE_PREFIX) {
- // Read the extended atom size.
+ if (atomSize == Atom.DEFINES_LARGE_SIZE) {
+ // Read the large size.
int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
atomHeaderBytesRead += headerBytesRemaining;
atomSize = atomHeader.readUnsignedLongToLong();
+ } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
+ // The atom extends to the end of the file. Note that if the atom is within a container we can
+ // work out its size even if the input length is unknown.
+ long endPosition = input.getLength();
+ if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) {
+ endPosition = containerAtoms.peek().endPosition;
+ }
+ if (endPosition != C.LENGTH_UNSET) {
+ atomSize = endPosition - input.getPosition() + atomHeaderBytesRead;
+ }
}
if (atomSize < atomHeaderBytesRead) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
index d0e770abdc..d3fe9e0d05 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -205,12 +205,26 @@ public final class Mp4Extractor implements Extractor, SeekMap {
atomType = atomHeader.readInt();
}
- if (atomSize == Atom.LONG_SIZE_PREFIX) {
- // Read the extended atom size.
+ if (atomSize == Atom.DEFINES_LARGE_SIZE) {
+ // Read the large size.
int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
atomHeaderBytesRead += headerBytesRemaining;
atomSize = atomHeader.readUnsignedLongToLong();
+ } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
+ // The atom extends to the end of the file. Note that if the atom is within a container we can
+ // work out its size even if the input length is unknown.
+ long endPosition = input.getLength();
+ if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) {
+ endPosition = containerAtoms.peek().endPosition;
+ }
+ if (endPosition != C.LENGTH_UNSET) {
+ atomSize = endPosition - input.getPosition() + atomHeaderBytesRead;
+ }
+ }
+
+ if (atomSize < atomHeaderBytesRead) {
+ throw new ParserException("Atom size less than header length (unsupported).");
}
if (shouldParseContainerAtom(atomType)) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
index 6d5c372619..cfca015348 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -31,22 +31,48 @@ public final class PsshAtomUtil {
private PsshAtomUtil() {}
/**
- * Builds a PSSH atom for a given {@link UUID} containing the given scheme specific data.
+ * Builds a version 0 PSSH atom for a given system id, containing the given data.
*
- * @param uuid The UUID of the scheme.
+ * @param systemId The system id of the scheme.
* @param data The scheme specific data.
* @return The PSSH atom.
*/
- public static byte[] buildPsshAtom(UUID uuid, byte[] data) {
- int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length;
+ public static byte[] buildPsshAtom(UUID systemId, byte[] data) {
+ return buildPsshAtom(systemId, null, data);
+ }
+
+ /**
+ * Builds a PSSH atom for the given system id, containing the given key ids and data.
+ *
+ * @param systemId The system id of the scheme.
+ * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom.
+ * @param data The scheme specific data.
+ * @return The PSSH atom.
+ */
+ public static byte[] buildPsshAtom(UUID systemId, UUID[] keyIds, byte[] data) {
+ boolean buildV1Atom = keyIds != null;
+ int dataLength = data != null ? data.length : 0;
+ int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength;
+ if (buildV1Atom) {
+ psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */;
+ }
ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength);
psshBox.putInt(psshBoxLength);
psshBox.putInt(Atom.TYPE_pssh);
- psshBox.putInt(0 /* version=0, flags=0 */);
- psshBox.putLong(uuid.getMostSignificantBits());
- psshBox.putLong(uuid.getLeastSignificantBits());
- psshBox.putInt(data.length);
- psshBox.put(data);
+ psshBox.putInt(buildV1Atom ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */);
+ psshBox.putLong(systemId.getMostSignificantBits());
+ psshBox.putLong(systemId.getLeastSignificantBits());
+ if (buildV1Atom) {
+ psshBox.putInt(keyIds.length);
+ for (UUID keyId : keyIds) {
+ psshBox.putLong(keyId.getMostSignificantBits());
+ psshBox.putLong(keyId.getLeastSignificantBits());
+ }
+ }
+ if (dataLength != 0) {
+ psshBox.putInt(data.length);
+ psshBox.put(data);
+ } // Else the last 4 bytes are a 0 DataSize.
return psshBox.array();
}
@@ -98,6 +124,7 @@ public final class PsshAtomUtil {
* @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is
* not a valid PSSH atom, or if the PSSH atom has an unsupported version.
*/
+ // TODO: Support parsing of the key ids for version 1 PSSH atoms.
private static Pair parsePsshAtom(byte[] atom) {
ParsableByteArray atomData = new ParsableByteArray(atom);
if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
index 44d5824945..021c9de654 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
@@ -104,11 +104,18 @@ import java.io.IOException;
input.peekFully(buffer.data, 0, headerSize);
long atomSize = buffer.readUnsignedInt();
int atomType = buffer.readInt();
- if (atomSize == Atom.LONG_SIZE_PREFIX) {
+ if (atomSize == Atom.DEFINES_LARGE_SIZE) {
+ // Read the large atom size.
headerSize = Atom.LONG_HEADER_SIZE;
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
buffer.setLimit(Atom.LONG_HEADER_SIZE);
atomSize = buffer.readUnsignedLongToLong();
+ } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
+ // The atom extends to the end of the file.
+ long endPosition = input.getLength();
+ if (endPosition != C.LENGTH_UNSET) {
+ atomSize = endPosition - input.getPosition() + headerSize;
+ }
}
if (atomSize < headerSize) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
index 7277df5bb8..96b964a4c4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -19,6 +19,7 @@ import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
@@ -128,7 +129,7 @@ public final class AdtsReader implements ElementaryStreamReader {
}
@Override
- public void consume(ParsableByteArray data) {
+ public void consume(ParsableByteArray data) throws ParserException {
while (data.bytesLeft() > 0) {
switch (state) {
case STATE_FINDING_SAMPLE:
@@ -276,7 +277,7 @@ public final class AdtsReader implements ElementaryStreamReader {
/**
* Parses the sample header.
*/
- private void parseAdtsHeader() {
+ private void parseAdtsHeader() throws ParserException {
adtsScratch.setPosition(0);
if (!hasOutputFormat) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
index 40cfd7f8d9..bd013f96a3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -94,9 +94,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_MPA:
case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
return new PesReader(new MpegAudioReader(esInfo.language));
- case TsExtractor.TS_STREAM_TYPE_AAC:
+ case TsExtractor.TS_STREAM_TYPE_AAC_ADTS:
return isSet(FLAG_IGNORE_AAC_STREAM)
? null : new PesReader(new AdtsReader(false, esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AAC_LATM:
+ return isSet(FLAG_IGNORE_AAC_STREAM)
+ ? null : new PesReader(new LatmReader(esInfo.language));
case TsExtractor.TS_STREAM_TYPE_AC3:
case TsExtractor.TS_STREAM_TYPE_E_AC3:
return new PesReader(new Ac3Reader(esInfo.language));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
index 57bcf31fc5..fa7f78c8c0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -50,8 +51,9 @@ public interface ElementaryStreamReader {
* Consumes (possibly partial) data from the current packet.
*
* @param data The data to consume.
+ * @throws ParserException If the data could not be parsed.
*/
- void consume(ParsableByteArray data);
+ void consume(ParsableByteArray data) throws ParserException;
/**
* Called when a packet ends.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
new file mode 100644
index 0000000000..d06c6f0cb4
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
@@ -0,0 +1,307 @@
+/*
+ * 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.extractor.ts;
+
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+
+/**
+ * Parses and extracts samples from an AAC/LATM elementary stream.
+ */
+public final class LatmReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_SYNC_1 = 0;
+ private static final int STATE_FINDING_SYNC_2 = 1;
+ private static final int STATE_READING_HEADER = 2;
+ private static final int STATE_READING_SAMPLE = 3;
+
+ private static final int INITIAL_BUFFER_SIZE = 1024;
+ private static final int SYNC_BYTE_FIRST = 0x56;
+ private static final int SYNC_BYTE_SECOND = 0xE0;
+
+ private final String language;
+ private final ParsableByteArray sampleDataBuffer;
+ private final ParsableBitArray sampleBitArray;
+
+ // Track output info.
+ private TrackOutput output;
+ private Format format;
+ private String formatId;
+
+ // Parser state info.
+ private int state;
+ private int bytesRead;
+ private int sampleSize;
+ private int secondHeaderByte;
+ private long timeUs;
+
+ // Container data.
+ private boolean streamMuxRead;
+ private int audioMuxVersion;
+ private int audioMuxVersionA;
+ private int numSubframes;
+ private int frameLengthType;
+ private boolean otherDataPresent;
+ private long otherDataLenBits;
+ private int sampleRateHz;
+ private long sampleDurationUs;
+ private int channelCount;
+
+ /**
+ * @param language Track language.
+ */
+ public LatmReader(@Nullable String language) {
+ this.language = language;
+ sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE);
+ sampleBitArray = new ParsableBitArray(sampleDataBuffer.data);
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC_1;
+ streamMuxRead = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ formatId = idGenerator.getFormatId();
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) throws ParserException {
+ int bytesToRead;
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC_1:
+ if (data.readUnsignedByte() == SYNC_BYTE_FIRST) {
+ state = STATE_FINDING_SYNC_2;
+ }
+ break;
+ case STATE_FINDING_SYNC_2:
+ int secondByte = data.readUnsignedByte();
+ if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) {
+ secondHeaderByte = secondByte;
+ state = STATE_READING_HEADER;
+ } else if (secondByte != SYNC_BYTE_FIRST) {
+ state = STATE_FINDING_SYNC_1;
+ }
+ break;
+ case STATE_READING_HEADER:
+ sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte();
+ if (sampleSize > sampleDataBuffer.data.length) {
+ resetBufferForSize(sampleSize);
+ }
+ bytesRead = 0;
+ state = STATE_READING_SAMPLE;
+ break;
+ case STATE_READING_SAMPLE:
+ bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ data.readBytes(sampleBitArray.data, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ sampleBitArray.setPosition(0);
+ parseAudioMuxElement(sampleBitArray);
+ state = STATE_FINDING_SYNC_1;
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41.
+ *
+ * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes.
+ */
+ private void parseAudioMuxElement(ParsableBitArray data) throws ParserException {
+ boolean useSameStreamMux = data.readBit();
+ if (!useSameStreamMux) {
+ streamMuxRead = true;
+ parseStreamMuxConfig(data);
+ } else if (!streamMuxRead) {
+ return; // Parsing cannot continue without StreamMuxConfig information.
+ }
+
+ if (audioMuxVersionA == 0) {
+ if (numSubframes != 0) {
+ throw new ParserException();
+ }
+ int muxSlotLengthBytes = parsePayloadLengthInfo(data);
+ parsePayloadMux(data, muxSlotLengthBytes);
+ if (otherDataPresent) {
+ data.skipBits((int) otherDataLenBits);
+ }
+ } else {
+ throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009.
+ }
+ }
+
+ /**
+ * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42.
+ */
+ private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException {
+ audioMuxVersion = data.readBits(1);
+ audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;
+ if (audioMuxVersionA == 0) {
+ if (audioMuxVersion == 1) {
+ latmGetValue(data); // Skip taraBufferFullness.
+ }
+ if (!data.readBit()) {
+ throw new ParserException();
+ }
+ numSubframes = data.readBits(6);
+ int numProgram = data.readBits(4);
+ int numLayer = data.readBits(3);
+ if (numProgram != 0 || numLayer != 0) {
+ throw new ParserException();
+ }
+ if (audioMuxVersion == 0) {
+ int startPosition = data.getPosition();
+ int readBits = parseAudioSpecificConfig(data);
+ data.setPosition(startPosition);
+ byte[] initData = new byte[(readBits + 7) / 8];
+ data.readBits(initData, 0, readBits);
+ Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz,
+ Collections.singletonList(initData), null, 0, language);
+ if (!format.equals(this.format)) {
+ this.format = format;
+ sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
+ output.format(format);
+ }
+ } else {
+ int ascLen = (int) latmGetValue(data);
+ int bitsRead = parseAudioSpecificConfig(data);
+ data.skipBits(ascLen - bitsRead); // fillBits.
+ }
+ parseFrameLength(data);
+ otherDataPresent = data.readBit();
+ otherDataLenBits = 0;
+ if (otherDataPresent) {
+ if (audioMuxVersion == 1) {
+ otherDataLenBits = latmGetValue(data);
+ } else {
+ boolean otherDataLenEsc;
+ do {
+ otherDataLenEsc = data.readBit();
+ otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8);
+ } while (otherDataLenEsc);
+ }
+ }
+ boolean crcCheckPresent = data.readBit();
+ if (crcCheckPresent) {
+ data.skipBits(8); // crcCheckSum.
+ }
+ } else {
+ throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009.
+ }
+ }
+
+ private void parseFrameLength(ParsableBitArray data) {
+ frameLengthType = data.readBits(3);
+ switch (frameLengthType) {
+ case 0:
+ data.skipBits(8); // latmBufferFullness.
+ break;
+ case 1:
+ data.skipBits(9); // frameLength.
+ break;
+ case 3:
+ case 4:
+ case 5:
+ data.skipBits(6); // CELPframeLengthTableIndex.
+ break;
+ case 6:
+ case 7:
+ data.skipBits(1); // HVXCframeLengthTableIndex.
+ break;
+ }
+ }
+
+ private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException {
+ int bitsLeft = data.bitsLeft();
+ Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true);
+ sampleRateHz = config.first;
+ channelCount = config.second;
+ return bitsLeft - data.bitsLeft();
+ }
+
+ private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException {
+ int muxSlotLengthBytes = 0;
+ // Assuming single program and single layer.
+ if (frameLengthType == 0) {
+ int tmp;
+ do {
+ tmp = data.readBits(8);
+ muxSlotLengthBytes += tmp;
+ } while (tmp == 255);
+ return muxSlotLengthBytes;
+ } else {
+ throw new ParserException();
+ }
+ }
+
+ private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) {
+ // The start of sample data in
+ int bitPosition = data.getPosition();
+ if ((bitPosition & 0x07) == 0) {
+ // Sample data is byte-aligned. We can output it directly.
+ sampleDataBuffer.setPosition(bitPosition >> 3);
+ } else {
+ // Sample data is not byte-aligned and we need align it ourselves before outputting.
+ // Byte alignment is needed because LATM framing is not supported by MediaCodec.
+ data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8);
+ sampleDataBuffer.setPosition(0);
+ }
+ output.sampleData(sampleDataBuffer, muxLengthBytes);
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null);
+ timeUs += sampleDurationUs;
+ }
+
+ private void resetBufferForSize(int newSize) {
+ sampleDataBuffer.reset(newSize);
+ sampleBitArray.reset(sampleDataBuffer.data);
+ }
+
+ private static long latmGetValue(ParsableBitArray data) {
+ int bytesForValue = data.readBits(2);
+ return data.readBits((bytesForValue + 1) * 8);
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
index 59696b9dea..4863df42eb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.Log;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -77,7 +78,8 @@ public final class PesReader implements TsPayloadReader {
}
@Override
- public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
+ public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
+ throws ParserException {
if (payloadUnitStartIndicator) {
switch (state) {
case STATE_FINDING_HEADER:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
index 883fb8f880..69c5745eaa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -30,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException;
/**
- * Facilitates the extraction of data from the MPEG-2 TS container format.
+ * Facilitates the extraction of data from the MPEG-2 PS container format.
*/
public final class PsExtractor implements Extractor {
@@ -275,8 +276,9 @@ public final class PsExtractor implements Extractor {
* Consumes the payload of a PS packet.
*
* @param data The PES packet. The position will be set to the start of the payload.
+ * @throws ParserException If the payload could not be parsed.
*/
- public void consume(ParsableByteArray data) {
+ public void consume(ParsableByteArray data) throws ParserException {
data.readBytes(pesScratch.data, 0, 3);
pesScratch.setPosition(0);
parseHeader();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
index 2929b8a076..90506ab2f6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -84,7 +84,8 @@ public final class TsExtractor implements Extractor {
public static final int TS_STREAM_TYPE_MPA = 0x03;
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
- public static final int TS_STREAM_TYPE_AAC = 0x0F;
+ public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F;
+ public static final int TS_STREAM_TYPE_AAC_LATM = 0x11;
public static final int TS_STREAM_TYPE_AC3 = 0x81;
public static final int TS_STREAM_TYPE_DTS = 0x8A;
public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
index e7996c66c3..efa764b572 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -196,7 +197,8 @@ public interface TsPayloadReader {
*
* @param data The TS packet. The position will be set to the start of the payload.
* @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
+ * @throws ParserException If the payload could not be parsed.
*/
- void consume(ParsableByteArray data, boolean payloadUnitStartIndicator);
+ void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 01229c1104..31c6a824ef 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -169,7 +169,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32;
private final MediaCodecSelector mediaCodecSelector;
- private final DrmSessionManager drmSessionManager;
+ @Nullable private final DrmSessionManager drmSessionManager;
private final boolean playClearSamplesWithoutKeys;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer flagsOnlyBuffer;
@@ -223,7 +223,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys) {
super(trackType);
Assertions.checkState(Util.SDK_INT >= 16);
@@ -530,7 +530,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
while (feedInputBuffer()) {}
TraceUtil.endSection();
} else {
- skipSource(positionUs);
+ decoderCounters.skippedInputBufferCount += skipSource(positionUs);
// We need to read any format changes despite not having a codec so that drmSession can be
// updated, and so that we have the most recent format should the codec be initialized. We may
// also reach the end of the stream. Note that readSource will not read a sample into a
@@ -1090,7 +1090,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* @param drmInitData {@link DrmInitData} of the format to check for support.
* @return Whether the encryption scheme is supported, or true if {@code drmInitData} is null.
*/
- private static boolean isDrmSchemeSupported(DrmSessionManager drmSessionManager,
+ private static boolean isDrmSchemeSupported(@Nullable DrmSessionManager drmSessionManager,
@Nullable DrmInitData drmInitData) {
if (drmInitData == null) {
// Content is unencrypted.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index d3f3dae344..1073e8d9c1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -288,9 +288,11 @@ public final class MediaCodecUtil {
return false;
}
- // Work around https://github.com/google/ExoPlayer/issues/1528
+ // Work around https://github.com/google/ExoPlayer/issues/1528 and
+ // https://github.com/google/ExoPlayer/issues/3171
if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
- && "a70".equals(Util.DEVICE)) {
+ && ("a70".equals(Util.DEVICE)
+ || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) {
return false;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java
new file mode 100644
index 0000000000..b635cbc4b2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java
@@ -0,0 +1,30 @@
+/*
+ * 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.metadata;
+
+/**
+ * Receives metadata output.
+ */
+public interface MetadataOutput {
+
+ /**
+ * Called when there is metadata associated with current playback time.
+ *
+ * @param metadata The metadata.
+ */
+ void onMetadata(Metadata metadata);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
index 7ff426e2df..f46dd467c8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -33,18 +33,10 @@ import java.util.Arrays;
public final class MetadataRenderer extends BaseRenderer implements Callback {
/**
- * Receives output from a {@link MetadataRenderer}.
+ * @deprecated Use {@link MetadataOutput}.
*/
- public interface Output {
-
- /**
- * Called each time there is a metadata associated with current playback time.
- *
- * @param metadata The metadata.
- */
- void onMetadata(Metadata metadata);
-
- }
+ @Deprecated
+ public interface Output extends MetadataOutput {}
private static final int MSG_INVOKE_RENDERER = 0;
// TODO: Holding multiple pending metadata objects is temporary mitigation against
@@ -53,7 +45,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private static final int MAX_PENDING_METADATA_COUNT = 5;
private final MetadataDecoderFactory decoderFactory;
- private final Output output;
+ private final MetadataOutput output;
private final Handler outputHandler;
private final FormatHolder formatHolder;
private final MetadataInputBuffer buffer;
@@ -73,7 +65,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
* {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
* called directly on the player's internal rendering thread.
*/
- public MetadataRenderer(Output output, Looper outputLooper) {
+ public MetadataRenderer(MetadataOutput output, Looper outputLooper) {
this(output, outputLooper, MetadataDecoderFactory.DEFAULT);
}
@@ -86,7 +78,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
* called directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
*/
- public MetadataRenderer(Output output, Looper outputLooper,
+ public MetadataRenderer(MetadataOutput output, Looper outputLooper,
MetadataDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
index 12f58d9a21..a8c33b4625 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -286,8 +286,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
- public void skipData(long positionUs) {
- stream.skipData(startUs + positionUs);
+ public int skipData(long positionUs) {
+ return stream.skipData(startUs + positionUs);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index 8be6bf028f..2387b43d5e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
@@ -132,9 +131,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
/**
* Provides a clipped view of a specified timeline.
*/
- private static final class ClippingTimeline extends Timeline {
+ private static final class ClippingTimeline extends ForwardingTimeline {
- private final Timeline timeline;
private final long startUs;
private final long endUs;
@@ -147,6 +145,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
* of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end.
*/
public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
+ super(timeline);
Assertions.checkArgument(timeline.getWindowCount() == 1);
Assertions.checkArgument(timeline.getPeriodCount() == 1);
Window window = timeline.getWindow(0, new Window(), false);
@@ -161,26 +160,10 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
}
Period period = timeline.getPeriod(0, new Period());
Assertions.checkArgument(period.getPositionInWindowUs() == 0);
- this.timeline = timeline;
this.startUs = startUs;
this.endUs = resolvedEndUs;
}
- @Override
- public int getWindowCount() {
- return 1;
- }
-
- @Override
- public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
- return timeline.getNextWindowIndex(windowIndex, repeatMode);
- }
-
- @Override
- public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
- return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
- }
-
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
@@ -202,11 +185,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
return window;
}
- @Override
- public int getPeriodCount() {
- return 1;
- }
-
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
period = timeline.getPeriod(0, period, setIds);
@@ -214,11 +192,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
return period;
}
- @Override
- public int getIndexOfPeriod(Object uid) {
- return timeline.getIndexOfPeriod(uid);
- }
-
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
index b00732e839..c2d2e5f11e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
@@ -186,8 +186,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
- for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
- mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError();
+ for (int i = 0; i < mediaSourceHolders.size(); i++) {
+ mediaSourceHolders.get(i).mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@@ -221,8 +221,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
@Override
public void releaseSource() {
- for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
- mediaSourceHolder.mediaSource.releaseSource();
+ for (int i = 0; i < mediaSourceHolders.size(); i++) {
+ mediaSourceHolders.get(i).mediaSource.releaseSource();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java
index 7aab22d8a0..299b816cc8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java
@@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream {
}
@Override
- public void skipData(long positionUs) {
- // Do nothing.
+ public int skipData(long positionUs) {
+ return 0;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
index e7273f834b..511f7f4a8a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -238,7 +238,7 @@ import java.util.Arrays;
// sample queue, or if we haven't read anything from the queue since the previous seek
// (this case is common for sparse tracks such as metadata tracks). In all other cases a
// seek is required.
- seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
+ seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
&& sampleQueue.getReadIndex() != 0;
}
}
@@ -371,12 +371,13 @@ import java.util.Arrays;
lastSeekPositionUs);
}
- /* package */ void skipData(int track, long positionUs) {
+ /* package */ int skipData(int track, long positionUs) {
SampleQueue sampleQueue = sampleQueues[track];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.advanceToEnd();
+ return sampleQueue.advanceToEnd();
} else {
- sampleQueue.advanceTo(positionUs, true, true);
+ int skipCount = sampleQueue.advanceTo(positionUs, true, true);
+ return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
}
}
@@ -558,7 +559,8 @@ import java.util.Arrays;
for (int i = 0; i < trackCount; i++) {
SampleQueue sampleQueue = sampleQueues[i];
sampleQueue.rewind();
- boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false);
+ boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)
+ != SampleQueue.ADVANCE_FAILED;
// If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue
// is successful. We ignore whether seeks within non-AV queues are successful in this case, as
// they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
@@ -632,8 +634,8 @@ import java.util.Arrays;
}
@Override
- public void skipData(long positionUs) {
- ExtractorMediaPeriod.this.skipData(track, positionUs);
+ public int skipData(long positionUs) {
+ return ExtractorMediaPeriod.this.skipData(track, positionUs);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java
new file mode 100644
index 0000000000..4203abbf39
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java
@@ -0,0 +1,68 @@
+/*
+ * 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.source;
+
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+
+/**
+ * An overridable {@link Timeline} implementation forwarding all methods to another timeline.
+ */
+public abstract class ForwardingTimeline extends Timeline {
+
+ protected final Timeline timeline;
+
+ public ForwardingTimeline(Timeline timeline) {
+ this.timeline = timeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return timeline.getWindowCount();
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ return timeline.getNextWindowIndex(windowIndex, repeatMode);
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
+ return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return timeline.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ return timeline.getPeriod(periodIndex, period, setIds);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return timeline.getIndexOfPeriod(uid);
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
index a6e93a92b9..1795fe8045 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -160,53 +160,25 @@ public final class LoopingMediaSource implements MediaSource {
}
- private static final class InfinitelyLoopingTimeline extends Timeline {
+ private static final class InfinitelyLoopingTimeline extends ForwardingTimeline {
- private final Timeline childTimeline;
-
- public InfinitelyLoopingTimeline(Timeline childTimeline) {
- this.childTimeline = childTimeline;
- }
-
- @Override
- public int getWindowCount() {
- return childTimeline.getWindowCount();
+ public InfinitelyLoopingTimeline(Timeline timeline) {
+ super(timeline);
}
@Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
- int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode);
+ int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode);
return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex;
}
@Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
- int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode);
+ int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode);
return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1
: childPreviousWindowIndex;
}
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
- }
-
- @Override
- public int getPeriodCount() {
- return childTimeline.getPeriodCount();
- }
-
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- return childTimeline.getPeriod(periodIndex, period, setIds);
- }
-
- @Override
- public int getIndexOfPeriod(Object uid) {
- return childTimeline.getIndexOfPeriod(uid);
- }
-
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index 7a43dd7562..514b96ae8d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -16,12 +16,15 @@
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.io.IOException;
/**
- * A source of a single period of media.
+ * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All
+ * methods are called on the player's internal playback thread, as described in the
+ * {@link ExoPlayer} Javadoc.
*/
public interface MediaPeriod extends SequenceableLoader {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
index 790620a80c..11489cfbb8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -23,7 +23,19 @@ import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
/**
- * A source of media consisting of one or more {@link MediaPeriod}s.
+ * Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main
+ * responsibilities:
+ *
+ *
To provide the player with a {@link Timeline} defining the structure of its media, and to
+ * provide a new timeline whenever the structure of the media changes. The MediaSource provides
+ * these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener}
+ * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
+ *
To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
+ * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the
+ * player to load and read the media.
+ *
+ * All methods are called on the player's internal playback thread, as described in the
+ * {@link ExoPlayer} Javadoc.
*/
public interface MediaSource {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
index 03b2e3b715..d70c59b195 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
@@ -253,32 +253,35 @@ import com.google.android.exoplayer2.util.Util;
* @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
* end of the queue, by advancing the read position to the last sample (or keyframe) in the
* queue.
- * @return Whether the operation was a success. A successful advance is one in which the read
- * position was unchanged or advanced, and is now at a sample meeting the specified criteria.
+ * @return The number of samples that were skipped if the operation was successful, which may be
+ * equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A
+ * successful advance is one in which the read position was unchanged or advanced, and is now
+ * at a sample meeting the specified criteria.
*/
- public synchronized boolean advanceTo(long timeUs, boolean toKeyframe,
+ public synchronized int advanceTo(long timeUs, boolean toKeyframe,
boolean allowTimeBeyondBuffer) {
int relativeReadIndex = getRelativeIndex(readPosition);
if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]
|| (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
- return false;
+ return SampleQueue.ADVANCE_FAILED;
}
int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe);
if (offset == -1) {
- return false;
+ return SampleQueue.ADVANCE_FAILED;
}
readPosition += offset;
- return true;
+ return offset;
}
/**
* Advances the read position to the end of the queue.
+ *
+ * @return The number of samples that were skipped.
*/
- public synchronized void advanceToEnd() {
- if (!hasNextSample()) {
- return;
- }
+ public synchronized int advanceToEnd() {
+ int skipCount = length - readPosition;
readPosition = length;
+ return skipCount;
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
index c7bae8f8b4..b83cf7df5b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
@@ -49,6 +49,8 @@ public final class SampleQueue implements TrackOutput {
}
+ public static final int ADVANCE_FAILED = -1;
+
private static final int INITIAL_SCRATCH_SIZE = 32;
private final Allocator allocator;
@@ -255,9 +257,11 @@ public final class SampleQueue implements TrackOutput {
/**
* Advances the read position to the end of the queue.
+ *
+ * @return The number of samples that were skipped.
*/
- public void advanceToEnd() {
- metadataQueue.advanceToEnd();
+ public int advanceToEnd() {
+ return metadataQueue.advanceToEnd();
}
/**
@@ -268,10 +272,12 @@ public final class SampleQueue implements TrackOutput {
* time, rather than to any sample before or at that time.
* @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
* end of the queue, by advancing the read position to the last sample (or keyframe).
- * @return Whether the operation was a success. A successful advance is one in which the read
- * position was unchanged or advanced, and is now at a sample meeting the specified criteria.
+ * @return The number of samples that were skipped if the operation was successful, which may be
+ * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful
+ * advance is one in which the read position was unchanged or advanced, and is now at a sample
+ * meeting the specified criteria.
*/
- public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {
+ public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {
return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java
index dc58c29c22..06efc980e2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java
@@ -70,7 +70,8 @@ public interface SampleStream {
* {@code positionUs} is beyond it.
*
* @param positionUs The specified time.
+ * @return The number of samples that were skipped.
*/
- void skipData(long positionUs);
+ int skipData(long positionUs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java
new file mode 100644
index 0000000000..4307fd2c19
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java
@@ -0,0 +1,256 @@
+/*
+ * 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.source;
+
+import com.google.android.exoplayer2.C;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Shuffled order of indices.
+ */
+public interface ShuffleOrder {
+
+ /**
+ * The default {@link ShuffleOrder} implementation for random shuffle order.
+ */
+ class DefaultShuffleOrder implements ShuffleOrder {
+
+ private final Random random;
+ private final int[] shuffled;
+ private final int[] indexInShuffled;
+
+ /**
+ * Creates an instance with a specified length.
+ *
+ * @param length The length of the shuffle order.
+ */
+ public DefaultShuffleOrder(int length) {
+ this(length, new Random());
+ }
+
+ /**
+ * Creates an instance with a specified length and the specified random seed. Shuffle orders of
+ * the same length initialized with the same random seed are guaranteed to be equal.
+ *
+ * @param length The length of the shuffle order.
+ * @param randomSeed A random seed.
+ */
+ public DefaultShuffleOrder(int length, long randomSeed) {
+ this(length, new Random(randomSeed));
+ }
+
+ private DefaultShuffleOrder(int length, Random random) {
+ this(createShuffledList(length, random), random);
+ }
+
+ private DefaultShuffleOrder(int[] shuffled, Random random) {
+ this.shuffled = shuffled;
+ this.random = random;
+ this.indexInShuffled = new int[shuffled.length];
+ for (int i = 0; i < shuffled.length; i++) {
+ indexInShuffled[shuffled[i]] = i;
+ }
+ }
+
+ @Override
+ public int getLength() {
+ return shuffled.length;
+ }
+
+ @Override
+ public int getNextIndex(int index) {
+ int shuffledIndex = indexInShuffled[index];
+ return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousIndex(int index) {
+ int shuffledIndex = indexInShuffled[index];
+ return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getLastIndex() {
+ return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getFirstIndex() {
+ return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET;
+ }
+
+ @Override
+ public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
+ int[] insertionPoints = new int[insertionCount];
+ int[] insertionValues = new int[insertionCount];
+ for (int i = 0; i < insertionCount; i++) {
+ insertionPoints[i] = random.nextInt(shuffled.length + 1);
+ int swapIndex = random.nextInt(i + 1);
+ insertionValues[i] = insertionValues[swapIndex];
+ insertionValues[swapIndex] = i + insertionIndex;
+ }
+ Arrays.sort(insertionPoints);
+ int[] newShuffled = new int[shuffled.length + insertionCount];
+ int indexInOldShuffled = 0;
+ int indexInInsertionList = 0;
+ for (int i = 0; i < shuffled.length + insertionCount; i++) {
+ if (indexInInsertionList < insertionCount
+ && indexInOldShuffled == insertionPoints[indexInInsertionList]) {
+ newShuffled[i] = insertionValues[indexInInsertionList++];
+ } else {
+ newShuffled[i] = shuffled[indexInOldShuffled++];
+ if (newShuffled[i] >= insertionIndex) {
+ newShuffled[i] += insertionCount;
+ }
+ }
+ }
+ return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
+ }
+
+ @Override
+ public ShuffleOrder cloneAndRemove(int removalIndex) {
+ int[] newShuffled = new int[shuffled.length - 1];
+ boolean foundRemovedElement = false;
+ for (int i = 0; i < shuffled.length; i++) {
+ if (shuffled[i] == removalIndex) {
+ foundRemovedElement = true;
+ } else {
+ newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex
+ ? shuffled[i] - 1 : shuffled[i];
+ }
+ }
+ return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
+ }
+
+ private static int[] createShuffledList(int length, Random random) {
+ int[] shuffled = new int[length];
+ for (int i = 0; i < length; i++) {
+ int swapIndex = random.nextInt(i + 1);
+ shuffled[i] = shuffled[swapIndex];
+ shuffled[swapIndex] = i;
+ }
+ return shuffled;
+ }
+
+ }
+
+ /**
+ * A {@link ShuffleOrder} implementation which does not shuffle.
+ */
+ final class UnshuffledShuffleOrder implements ShuffleOrder {
+
+ private final int length;
+
+ /**
+ * Creates an instance with a specified length.
+ *
+ * @param length The length of the shuffle order.
+ */
+ public UnshuffledShuffleOrder(int length) {
+ this.length = length;
+ }
+
+ @Override
+ public int getLength() {
+ return length;
+ }
+
+ @Override
+ public int getNextIndex(int index) {
+ return ++index < length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousIndex(int index) {
+ return --index >= 0 ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getLastIndex() {
+ return length > 0 ? length - 1 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getFirstIndex() {
+ return length > 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
+ return new UnshuffledShuffleOrder(length + insertionCount);
+ }
+
+ @Override
+ public ShuffleOrder cloneAndRemove(int removalIndex) {
+ return new UnshuffledShuffleOrder(length - 1);
+ }
+
+ }
+
+ /**
+ * Returns length of shuffle order.
+ */
+ int getLength();
+
+ /**
+ * Returns the next index in the shuffle order.
+ *
+ * @param index An index.
+ * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last
+ * element.
+ */
+ int getNextIndex(int index);
+
+ /**
+ * Returns the previous index in the shuffle order.
+ *
+ * @param index An index.
+ * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first
+ * element.
+ */
+ int getPreviousIndex(int index);
+
+ /**
+ * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
+ * empty.
+ */
+ int getLastIndex();
+
+ /**
+ * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
+ * empty.
+ */
+ int getFirstIndex();
+
+ /**
+ * Return a copy of the shuffle order with newly inserted elements.
+ *
+ * @param insertionIndex The index in the unshuffled order at which elements are inserted.
+ * @param insertionCount The number of elements inserted at {@code insertionIndex}.
+ * @return A copy of this {@link ShuffleOrder} with newly inserted elements.
+ */
+ ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);
+
+ /**
+ * Return a copy of the shuffle order with one element removed.
+ *
+ * @param removalIndex The index of the element in the unshuffled order which is to be removed.
+ * @return A copy of this {@link ShuffleOrder} without the removed element.
+ */
+ ShuffleOrder cloneAndRemove(int removalIndex);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 3435c01eeb..b19f398d86 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -235,10 +235,12 @@ import java.util.Arrays;
}
@Override
- public void skipData(long positionUs) {
- if (positionUs > 0) {
+ public int skipData(long positionUs) {
+ if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) {
streamState = STREAM_STATE_END_OF_STREAM;
+ return 1;
}
+ return 0;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index 0fc3d5881e..f2609a0ffd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -160,6 +160,7 @@ public class ChunkSampleStream implements SampleStream, S
* @return An estimate of the absolute position in microseconds up to which data is buffered, or
* {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
*/
+ @Override
public long getBufferedPositionUs() {
if (loadingFinished) {
return C.TIME_END_OF_SOURCE;
@@ -185,8 +186,8 @@ public class ChunkSampleStream implements SampleStream, S
public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the primary sample queue.
- boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true,
- positionUs < getNextLoadPositionUs());
+ boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true,
+ positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED);
if (seekInsideBuffer) {
// We succeeded. Discard samples and corresponding chunks prior to the seek position.
discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
@@ -266,13 +267,19 @@ public class ChunkSampleStream implements SampleStream, S
}
@Override
- public void skipData(long positionUs) {
+ public int skipData(long positionUs) {
+ int skipCount;
if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
primarySampleQueue.advanceToEnd();
+ skipCount = primarySampleQueue.advanceToEnd();
} else {
- primarySampleQueue.advanceTo(positionUs, true, true);
+ skipCount = primarySampleQueue.advanceTo(positionUs, true, true);
+ if (skipCount == SampleQueue.ADVANCE_FAILED) {
+ skipCount = 0;
+ }
}
primarySampleQueue.discardToRead();
+ return skipCount;
}
// Loader.Callback implementation.
@@ -470,11 +477,12 @@ public class ChunkSampleStream implements SampleStream, S
}
@Override
- public void skipData(long positionUs) {
+ public int skipData(long positionUs) {
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.advanceToEnd();
+ return sampleQueue.advanceToEnd();
} else {
- sampleQueue.advanceTo(positionUs, true, true);
+ int skipCount = sampleQueue.advanceTo(positionUs, true, true);
+ return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java
new file mode 100644
index 0000000000..5a08db94cb
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java
@@ -0,0 +1,32 @@
+/*
+ * 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.text;
+
+import java.util.List;
+
+/**
+ * Receives text output.
+ */
+public interface TextOutput {
+
+ /**
+ * Called when there is a change in the {@link Cue}s.
+ *
+ * @param cues The {@link Cue}s.
+ */
+ void onCues(List cues);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
index 1820d43e75..8e1966305e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
@@ -37,23 +37,15 @@ import java.util.List;
*
* {@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances obtained
* from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s is
- * delegated to an {@link Output}.
+ * delegated to an {@link TextOutput}.
*/
public final class TextRenderer extends BaseRenderer implements Callback {
/**
- * Receives output from a {@link TextRenderer}.
+ * @deprecated Use {@link TextOutput}.
*/
- public interface Output {
-
- /**
- * Called each time there is a change in the {@link Cue}s.
- *
- * @param cues The {@link Cue}s.
- */
- void onCues(List cues);
-
- }
+ @Deprecated
+ public interface Output extends TextOutput {}
@Retention(RetentionPolicy.SOURCE)
@IntDef({REPLACEMENT_STATE_NONE, REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
@@ -79,7 +71,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
private static final int MSG_UPDATE_OUTPUT = 0;
private final Handler outputHandler;
- private final Output output;
+ private final TextOutput output;
private final SubtitleDecoderFactory decoderFactory;
private final FormatHolder formatHolder;
@@ -101,7 +93,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
* using {@link android.app.Activity#getMainLooper()}. Null may be passed if the output
* should be called directly on the player's internal rendering thread.
*/
- public TextRenderer(Output output, Looper outputLooper) {
+ public TextRenderer(TextOutput output, Looper outputLooper) {
this(output, outputLooper, SubtitleDecoderFactory.DEFAULT);
}
@@ -114,7 +106,8 @@ public final class TextRenderer extends BaseRenderer implements Callback {
* should be called directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.
*/
- public TextRenderer(Output output, Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
+ public TextRenderer(TextOutput output, Looper outputLooper,
+ SubtitleDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_TEXT);
this.output = Assertions.checkNotNull(output);
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
index 8fd70f7a67..030f0cdbb0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
@@ -811,43 +811,43 @@ public final class Cea708Decoder extends CeaDecoder {
private static final int PEN_OFFSET_NORMAL = 1;
// The window style properties are specified in the CEA-708 specification.
- private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{
+ private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] {
JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
JUSTIFICATION_LEFT
};
- private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{
+ private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] {
DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
DIRECTION_TOP_TO_BOTTOM
};
- private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{
+ private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] {
DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
DIRECTION_RIGHT_TO_LEFT
};
- private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{
+ private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] {
false, false, false, true, true, true, false
};
- private static final int[] WINDOW_STYLE_FILL = new int[]{
+ private static final int[] WINDOW_STYLE_FILL = new int[] {
COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK
};
// The pen style properties are specified in the CEA-708 specification.
- private static final int[] PEN_STYLE_FONT_STYLE = new int[]{
+ private static final int[] PEN_STYLE_FONT_STYLE = new int[] {
PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS
};
- private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{
+ private static final int[] PEN_STYLE_EDGE_TYPE = new int[] {
BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,
BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
BORDER_AND_EDGE_TYPE_UNIFORM
};
- private static final int[] PEN_STYLE_BACKGROUND = new int[]{
+ private static final int[] PEN_STYLE_BACKGROUND = new int[] {
COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
index e76f0fd7e2..6cce902e87 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
@@ -69,6 +69,11 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
// Read and parse the timing line.
boolean haveEndTimecode = false;
currentLine = subripData.readLine();
+ if (currentLine == null) {
+ Log.w(TAG, "Unexpected end");
+ break;
+ }
+
Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
if (matcher.matches()) {
cueTimesUs.add(parseTimecode(matcher, 1));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
index 45ac9eab6e..d518b5a6be 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -199,6 +199,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
* @param trackIndex The index of the track within the track group.
* @return One of {@link RendererCapabilities#FORMAT_HANDLED},
* {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM},
* {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and
* {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}.
*/
@@ -214,6 +215,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
* Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
* {@link RendererCapabilities#FORMAT_HANDLED} are always considered.
* Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM},
* {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or
* {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.
* Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
@@ -615,12 +617,12 @@ public abstract class MappingTrackSelector extends TrackSelector {
/**
* Finds the renderer to which the provided {@link TrackGroup} should be mapped.
*
- * A {@link TrackGroup} is mapped to the renderer that reports
- * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group,
- * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or
- * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In
- * the case that two or more renderers report the same level of support, the renderer with the
- * lowest index is associated.
+ * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in
+ * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED},
+ * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers
+ * report the same level of support, the renderer with the lowest index is associated.
*
* If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the
* tracks in the group, then {@code renderers.length} is returned to indicate that the group was
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
new file mode 100644
index 0000000000..c547625819
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
@@ -0,0 +1,88 @@
+/*
+ * 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.upstream;
+
+import android.net.Uri;
+import android.util.Base64;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import java.io.IOException;
+import java.net.URLDecoder;
+
+/**
+ * A {@link DataSource} for reading data URLs, as defined by RFC 2397.
+ */
+public final class DataSchemeDataSource implements DataSource {
+
+ public static final String SCHEME_DATA = "data";
+
+ private DataSpec dataSpec;
+ private int bytesRead;
+ private byte[] data;
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ this.dataSpec = dataSpec;
+ Uri uri = dataSpec.uri;
+ String scheme = uri.getScheme();
+ if (!SCHEME_DATA.equals(scheme)) {
+ throw new ParserException("Unsupported scheme: " + scheme);
+ }
+ String[] uriParts = uri.getSchemeSpecificPart().split(",");
+ if (uriParts.length > 2) {
+ throw new ParserException("Unexpected URI format: " + uri);
+ }
+ String dataString = uriParts[1];
+ if (uriParts[0].contains(";base64")) {
+ try {
+ data = Base64.decode(dataString, 0);
+ } catch (IllegalArgumentException e) {
+ throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
+ }
+ } else {
+ // TODO: Add support for other charsets.
+ data = URLDecoder.decode(dataString, C.ASCII_NAME).getBytes();
+ }
+ return data.length;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) {
+ if (readLength == 0) {
+ return 0;
+ }
+ int remainingBytes = data.length - bytesRead;
+ if (remainingBytes == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = Math.min(readLength, remainingBytes);
+ System.arraycopy(data, bytesRead, buffer, offset, readLength);
+ bytesRead += readLength;
+ return readLength;
+ }
+
+ @Override
+ public Uri getUri() {
+ return dataSpec != null ? dataSpec.uri : null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ dataSpec = null;
+ data = null;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
index cbb8ba92a5..853b40f73f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
@@ -34,10 +34,11 @@ import java.lang.reflect.InvocationTargetException;
*
content: For fetching data from a content URI (e.g. content://authority/path/123).
*
rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
* explicit dependency on ExoPlayer's RTMP extension.
+ *
data: For parsing data inlined in the URI as defined in RFC 2397.
*
http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if
* constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or
* any other schemes supported by a base data source if constructed using
- * {@link #DefaultDataSource(Context, TransferListener, DataSource)}.
+ * {@link #DefaultDataSource(Context, TransferListener, DataSource)}.
*
*/
public final class DefaultDataSource implements DataSource {
@@ -58,6 +59,7 @@ public final class DefaultDataSource implements DataSource {
private DataSource assetDataSource;
private DataSource contentDataSource;
private DataSource rtmpDataSource;
+ private DataSource dataSchemeDataSource;
private DataSource dataSource;
@@ -130,6 +132,8 @@ public final class DefaultDataSource implements DataSource {
dataSource = getContentDataSource();
} else if (SCHEME_RTMP.equals(scheme)) {
dataSource = getRtmpDataSource();
+ } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
+ dataSource = getDataSchemeDataSource();
} else {
dataSource = baseDataSource;
}
@@ -202,4 +206,11 @@ public final class DefaultDataSource implements DataSource {
return rtmpDataSource;
}
+ private DataSource getDataSchemeDataSource() {
+ if (dataSchemeDataSource == null) {
+ dataSchemeDataSource = new DataSchemeDataSource();
+ }
+ return dataSchemeDataSource;
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
index adf245d9aa..308340b8b2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
@@ -72,8 +72,19 @@ public final class ParsingLoadable implements Loadable {
* @param parser Parses the object from the response.
*/
public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser extends T> parser) {
+ this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser);
+ }
+
+ /**
+ * @param dataSource A {@link DataSource} to use when loading the data.
+ * @param dataSpec The {@link DataSpec} from which the object should be loaded.
+ * @param type See {@link #type}.
+ * @param parser Parses the object from the response.
+ */
+ public ParsingLoadable(DataSource dataSource, DataSpec dataSpec, int type,
+ Parser extends T> parser) {
this.dataSource = dataSource;
- this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP);
+ this.dataSpec = dataSpec;
this.type = type;
this.parser = parser;
}
@@ -108,7 +119,7 @@ public final class ParsingLoadable implements Loadable {
}
@Override
- public final void load() throws IOException, InterruptedException {
+ public final void load() throws IOException {
DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
try {
inputStream.open();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
index 86ff810142..0265ef83ea 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import android.support.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.NavigableSet;
@@ -104,7 +105,7 @@ public interface Cache {
* @param key The key for which spans should be returned.
* @return The spans for the key. May be null if there are no such spans.
*/
- NavigableSet getCachedSpans(String key);
+ @Nullable NavigableSet getCachedSpans(String key);
/**
* Returns all keys in the cache.
@@ -151,7 +152,7 @@ public interface Cache {
* @param position The position of the data being requested.
* @return The {@link CacheSpan}. Or null if the cache entry is locked.
*/
- CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
+ @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
/**
* Obtains a cache file into which data can be written. Must only be called when holding a
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
index 97d55c5fe2..2082740bb4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.io.File;
@@ -43,7 +44,7 @@ public class CacheSpan implements Comparable {
/**
* The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false.
*/
- public final File file;
+ public final @Nullable File file;
/**
* The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false.
*/
@@ -68,11 +69,12 @@ public class CacheSpan implements Comparable {
* @param position The position of the {@link CacheSpan} in the original stream.
* @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
* open-ended hole.
- * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if
- * {@link #isCached} is false.
+ * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link
+ * #isCached} is false.
* @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
*/
- public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) {
+ public CacheSpan(
+ String key, long position, long length, long lastAccessTimestamp, @Nullable File file) {
this.key = key;
this.position = position;
this.length = length;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
index 58cc70d68d..e1c2c13865 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -34,9 +34,9 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.Random;
import java.util.Set;
import javax.crypto.Cipher;
@@ -64,6 +64,7 @@ import javax.crypto.spec.SecretKeySpec;
private final AtomicFile atomicFile;
private final Cipher cipher;
private final SecretKeySpec secretKeySpec;
+ private final boolean encrypt;
private boolean changed;
private ReusableBufferedOutputStream bufferedOutputStream;
@@ -80,10 +81,21 @@ import javax.crypto.spec.SecretKeySpec;
* Creates a CachedContentIndex which works on the index file in the given cacheDir.
*
* @param cacheDir Directory where the index file is kept.
- * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
- * The key must be 16 bytes long.
+ * @param secretKey 16 byte AES key for reading and writing the cache index.
*/
public CachedContentIndex(File cacheDir, byte[] secretKey) {
+ this(cacheDir, secretKey, secretKey != null);
+ }
+
+ /**
+ * Creates a CachedContentIndex which works on the index file in the given cacheDir.
+ *
+ * @param cacheDir Directory where the index file is kept.
+ * @param secretKey 16 byte AES key for reading, and optionally writing, the cache index.
+ * @param encrypt When false, a plaintext index will be written.
+ */
+ public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) {
+ this.encrypt = encrypt;
if (secretKey != null) {
Assertions.checkArgument(secretKey.length == 16);
try {
@@ -176,14 +188,14 @@ import javax.crypto.spec.SecretKeySpec;
/** Removes empty {@link CachedContent} instances from index. */
public void removeEmpty() {
- LinkedList cachedContentToBeRemoved = new LinkedList<>();
+ ArrayList cachedContentToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : keyToContent.values()) {
if (cachedContent.isEmpty()) {
cachedContentToBeRemoved.add(cachedContent.key);
}
}
- for (String key : cachedContentToBeRemoved) {
- removeEmpty(key);
+ for (int i = 0; i < cachedContentToBeRemoved.size(); i++) {
+ removeEmpty(cachedContentToBeRemoved.get(i));
}
}
@@ -288,10 +300,11 @@ import javax.crypto.spec.SecretKeySpec;
output = new DataOutputStream(bufferedOutputStream);
output.writeInt(VERSION);
- int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0;
+ boolean writeEncrypted = encrypt && cipher != null;
+ int flags = writeEncrypted ? FLAG_ENCRYPTED_INDEX : 0;
output.writeInt(flags);
- if (cipher != null) {
+ if (writeEncrypted) {
byte[] initializationVector = new byte[16];
new Random().nextBytes(initializationVector);
output.write(initializationVector);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
index 2da6ba759b..bb1ac83698 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -22,7 +22,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
@@ -61,10 +60,24 @@ public final class SimpleCache implements Cache {
* The key must be 16 bytes long.
*/
public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) {
+ this(cacheDir, evictor, secretKey, secretKey != null);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used.
+ * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+ * The key must be 16 bytes long.
+ * @param encrypt When false, a plaintext index will be written.
+ */
+ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) {
this.cacheDir = cacheDir;
this.evictor = evictor;
this.lockedSpans = new HashMap<>();
- this.index = new CachedContentIndex(cacheDir, secretKey);
+ this.index = new CachedContentIndex(cacheDir, secretKey, encrypt);
this.listeners = new HashMap<>();
// Start cache initialization.
final ConditionVariable conditionVariable = new ConditionVariable();
@@ -308,7 +321,7 @@ public final class SimpleCache implements Cache {
* no longer exist.
*/
private void removeStaleSpansAndCachedContents() throws CacheException {
- LinkedList spansToBeRemoved = new LinkedList<>();
+ ArrayList spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) {
if (!span.file.exists()) {
@@ -316,9 +329,9 @@ public final class SimpleCache implements Cache {
}
}
}
- for (CacheSpan span : spansToBeRemoved) {
+ for (int i = 0; i < spansToBeRemoved.size(); i++) {
// Remove span but not CachedContent to prevent multiple index.store() calls.
- removeSpan(span, false);
+ removeSpan(spansToBeRemoved.get(i), false);
}
index.removeEmpty();
index.store();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java
index f2e30d981b..e85c07fba9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.google.android.exoplayer2.util;
import android.support.annotation.NonNull;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
index 0093c3b826..0514d9dbdc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util;
import android.util.Pair;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import java.util.ArrayList;
import java.util.List;
@@ -83,11 +84,27 @@ public final class CodecSpecificDataUtil {
/**
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
*
- * @param audioSpecificConfig The AudioSpecificConfig to parse.
+ * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse.
* @return A pair consisting of the sample rate in Hz and the channel count.
+ * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.
*/
- public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) {
- ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig);
+ public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig)
+ throws ParserException {
+ return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false);
+ }
+
+ /**
+ * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The
+ * position is advanced to the end of the AudioSpecificConfig.
+ * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for
+ * knowing the length of the configuration payload.
+ * @return A pair consisting of the sample rate in Hz and the channel count.
+ * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.
+ */
+ public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray,
+ boolean forceReadToEnd) throws ParserException {
int audioObjectType = getAacAudioObjectType(bitArray);
int sampleRate = getAacSamplingFrequency(bitArray);
int channelConfiguration = bitArray.readBits(4);
@@ -104,6 +121,41 @@ public final class CodecSpecificDataUtil {
channelConfiguration = bitArray.readBits(4);
}
}
+
+ if (forceReadToEnd) {
+ switch (audioObjectType) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 6:
+ case 7:
+ case 17:
+ case 19:
+ case 20:
+ case 21:
+ case 22:
+ case 23:
+ parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration);
+ break;
+ default:
+ throw new ParserException("Unsupported audio object type: " + audioObjectType);
+ }
+ switch (audioObjectType) {
+ case 17:
+ case 19:
+ case 20:
+ case 21:
+ case 22:
+ case 23:
+ int epConfig = bitArray.readBits(2);
+ if (epConfig == 2 || epConfig == 3) {
+ throw new ParserException("Unsupported epConfig: " + epConfig);
+ }
+ break;
+ }
+ }
+ // For supported containers, bits_to_decode() is always 0.
int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration];
Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID);
return Pair.create(sampleRate, channelCount);
@@ -269,4 +321,32 @@ public final class CodecSpecificDataUtil {
return samplingFrequency;
}
+ private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType,
+ int channelConfiguration) {
+ bitArray.skipBits(1); // frameLengthFlag.
+ boolean dependsOnCoreDecoder = bitArray.readBit();
+ if (dependsOnCoreDecoder) {
+ bitArray.skipBits(14); // coreCoderDelay.
+ }
+ boolean extensionFlag = bitArray.readBit();
+ if (channelConfiguration == 0) {
+ throw new UnsupportedOperationException(); // TODO: Implement programConfigElement();
+ }
+ if (audioObjectType == 6 || audioObjectType == 20) {
+ bitArray.skipBits(3); // layerNr.
+ }
+ if (extensionFlag) {
+ if (audioObjectType == 22) {
+ bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11).
+ }
+ if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20
+ || audioObjectType == 23) {
+ // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag,
+ // aacSpectralDataResilienceFlag.
+ bitArray.skipBits(3);
+ }
+ bitArray.skipBits(1); // extensionFlag3.
+ }
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java
new file mode 100644
index 0000000000..3d2c043a91
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java
@@ -0,0 +1,33 @@
+/*
+ * 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.util;
+
+import android.util.Pair;
+
+/**
+ * Converts exceptions into error codes and user readable error messages.
+ */
+public interface ErrorMessageProvider {
+
+ /**
+ * Returns a pair consisting of an error code and a user readable error message for the given
+ * exception.
+ *
+ * @param exception The exception for which an error code and message should be generated.
+ */
+ Pair getErrorMessage(T exception);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java
index 199ceff892..fdee7fb5e6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java
@@ -174,6 +174,43 @@ public final class ParsableBitArray {
return returnValue;
}
+ /**
+ * Reads {@code numBits} bits into {@code buffer}.
+ *
+ * @param buffer The array into which the read data should be written. The trailing
+ * {@code numBits % 8} bits are written into the most significant bits of the last modified
+ * {@code buffer} byte. The remaining ones are unmodified.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param numBits The number of bits to read.
+ */
+ public void readBits(byte[] buffer, int offset, int numBits) {
+ // Whole bytes.
+ int to = offset + (numBits >> 3) /* numBits / 8 */;
+ for (int i = offset; i < to; i++) {
+ buffer[i] = (byte) (data[byteOffset++] << bitOffset);
+ buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
+ }
+ // Trailing bits.
+ int bitsLeft = numBits & 7 /* numBits % 8 */;
+ if (bitsLeft == 0) {
+ return;
+ }
+ buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten.
+ if (bitOffset + bitsLeft > 8) {
+ // We read the rest of data[byteOffset] and increase byteOffset.
+ buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset);
+ bitOffset -= 8;
+ }
+ bitOffset += bitsLeft;
+ int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset);
+ buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft));
+ if (bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset++;
+ }
+ assertValidOffset();
+ }
+
/**
* Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
index e32f23fed7..a1820ed7a1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
@@ -34,6 +34,7 @@ import static android.opengl.EGL14.EGL_WINDOW_BIT;
import static android.opengl.EGL14.eglChooseConfig;
import static android.opengl.EGL14.eglCreateContext;
import static android.opengl.EGL14.eglCreatePbufferSurface;
+import static android.opengl.EGL14.eglDestroyContext;
import static android.opengl.EGL14.eglGetDisplay;
import static android.opengl.EGL14.eglInitialize;
import static android.opengl.EGL14.eglMakeCurrent;
@@ -42,7 +43,6 @@ import static android.opengl.GLES20.glGenTextures;
import android.annotation.TargetApi;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.opengl.EGL14;
@@ -152,15 +152,9 @@ public final class DummySurface extends Surface {
*
* @param context Any {@link Context}.
*/
+ @SuppressWarnings("unused") // Context may be needed in the future for better targeting.
private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) {
- return Util.SDK_INT == 24
- && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955"))
- && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager());
- }
-
- @TargetApi(24)
- private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) {
- return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
+ return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER);
}
private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
@@ -171,6 +165,8 @@ public final class DummySurface extends Surface {
private static final int MSG_RELEASE = 3;
private final int[] textureIdHolder;
+ private EGLContext context;
+ private EGLDisplay display;
private Handler handler;
private SurfaceTexture surfaceTexture;
@@ -255,7 +251,7 @@ public final class DummySurface extends Surface {
}
private void initInternal(boolean secure) {
- EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
Assertions.checkState(display != null, "eglGetDisplay failed");
int[] version = new int[2];
@@ -292,8 +288,8 @@ public final class DummySurface extends Surface {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE};
}
- EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT,
- glAttributes, 0);
+ context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes,
+ 0);
Assertions.checkState(context != null, "eglCreateContext failed");
int[] pbufferAttributes;
@@ -323,11 +319,18 @@ public final class DummySurface extends Surface {
private void releaseInternal() {
try {
- surfaceTexture.release();
+ if (surfaceTexture != null) {
+ surfaceTexture.release();
+ glDeleteTextures(1, textureIdHolder, 0);
+ }
} finally {
+ if (context != null) {
+ eglDestroyContext(display, context);
+ }
+ display = null;
+ context = null;
surface = null;
surfaceTexture = null;
- glDeleteTextures(1, textureIdHolder, 0);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index 9a2927cc3f..8fe3476351 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -26,6 +26,7 @@ import android.media.MediaFormat;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.C;
@@ -137,8 +138,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
- long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener,
- int maxDroppedFrameCountToNotify) {
+ long allowedJoiningTimeMs, @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) {
this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler,
eventListener, maxDroppedFrameCountToNotify);
}
@@ -162,9 +163,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
- long allowedJoiningTimeMs, DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, Handler eventHandler,
- VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {
+ long allowedJoiningTimeMs,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {
super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
index 53d6a76b8d..d6ea0ebae2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.video;
import android.os.Handler;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import android.view.Surface;
import android.view.TextureView;
import com.google.android.exoplayer2.Format;
@@ -109,15 +110,16 @@ public interface VideoRendererEventListener {
*/
final class EventDispatcher {
- private final Handler handler;
- private final VideoRendererEventListener listener;
+ @Nullable private final Handler handler;
+ @Nullable private final VideoRendererEventListener listener;
/**
* @param handler A handler for dispatching events, or null if creating a dummy instance.
* @param listener The listener to which events should be dispatched, or null if creating a
* dummy instance.
*/
- public EventDispatcher(Handler handler, VideoRendererEventListener listener) {
+ public EventDispatcher(@Nullable Handler handler,
+ @Nullable VideoRendererEventListener listener) {
this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
this.listener = listener;
}
diff --git a/library/dash/README.md b/library/dash/README.md
new file mode 100644
index 0000000000..394a38a332
--- /dev/null
+++ b/library/dash/README.md
@@ -0,0 +1,12 @@
+# ExoPlayer DASH library module #
+
+Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To
+play DASH content, instantiate a `DashMediaSource` and pass it to
+`ExoPlayer.prepare`.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*`
+ belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/library/dash/build.gradle b/library/dash/build.gradle
index 2220e5b250..48e9b9b97e 100644
--- a/library/dash/build.gradle
+++ b/library/dash/build.gradle
@@ -35,7 +35,6 @@ android {
dependencies {
compile project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion
- compile 'com.android.support:support-core-utils:' + supportLibraryVersion
androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/androidTest/AndroidManifest.xml
index a9b143253f..3a5b0c1fa2 100644
--- a/library/dash/src/androidTest/AndroidManifest.xml
+++ b/library/dash/src/androidTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.source.dash.test">
-
+ loadable = new ParsingLoadable<>(dataSource, dataSpec,
+ C.DATA_TYPE_MANIFEST, new DashManifestParser());
+ loadable.load();
+ return loadable.getResult();
}
/**
- * Loads {@link DrmInitData} for a given manifest.
- *
- * @param dataSource The {@link HttpDataSource} from which data should be loaded.
- * @param dashManifest The {@link DashManifest} of the DASH content.
- * @return The loaded {@link DrmInitData}.
- */
- public static DrmInitData loadDrmInitData(DataSource dataSource, DashManifest dashManifest)
- throws IOException, InterruptedException {
- // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
- // as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
- if (dashManifest.getPeriodCount() < 1) {
- return null;
- }
- Period period = dashManifest.getPeriod(0);
- int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);
- if (adaptationSetIndex == C.INDEX_UNSET) {
- adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO);
- if (adaptationSetIndex == C.INDEX_UNSET) {
- return null;
- }
- }
- AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
- if (adaptationSet.representations.isEmpty()) {
- return null;
- }
- Representation representation = adaptationSet.representations.get(0);
- DrmInitData drmInitData = representation.format.drmInitData;
- if (drmInitData == null) {
- Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation);
- if (sampleFormat != null) {
- drmInitData = sampleFormat.drmInitData;
- }
- if (drmInitData == null) {
- return null;
- }
- }
- return drmInitData;
- }
-
- /**
- * Loads initialization data for the {@code representation} and returns the sample {@link
- * Format}.
* Loads {@link DrmInitData} for a given period in a DASH manifest.
*
* @param dataSource The {@link HttpDataSource} from which data should be loaded.
@@ -157,7 +111,8 @@ public final class DashUtil {
*
* @param dataSource The source from which the data should be loaded.
* @param representation The representation which initialization chunk belongs to.
- * @return {@link ChunkIndex} of the given representation.
+ * @return The {@link ChunkIndex} of the given representation, or null if no initialization or
+ * index data exists.
* @throws IOException Thrown when there is an error while loading.
* @throws InterruptedException Thrown if the thread was interrupted.
*/
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
index 297052f65a..dd62d47621 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
@@ -85,14 +85,14 @@ public class DefaultDashChunkSource implements DashChunkSource {
private final int[] adaptationSetIndices;
private final TrackSelection trackSelection;
private final int trackType;
- private final RepresentationHolder[] representationHolders;
private final DataSource dataSource;
private final long elapsedRealtimeOffsetMs;
private final int maxSegmentsPerLoad;
+ protected final RepresentationHolder[] representationHolders;
+
private DashManifest manifest;
private int periodIndex;
-
private IOException fatalError;
private boolean missingLastSegment;
@@ -377,9 +377,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
// Protected classes.
+ /**
+ * Holds information about a single {@link Representation}.
+ */
protected static final class RepresentationHolder {
- public final ChunkExtractorWrapper extractorWrapper;
+ /* package */ final ChunkExtractorWrapper extractorWrapper;
public Representation representation;
public DashSegmentIndex segmentIndex;
@@ -387,7 +390,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
private long periodDurationUs;
private int segmentNumShift;
- public RepresentationHolder(long periodDurationUs, Representation representation,
+ /* package */ RepresentationHolder(long periodDurationUs, Representation representation,
boolean enableEventMessageTrack, boolean enableCea608Track) {
this.periodDurationUs = periodDurationUs;
this.representation = representation;
@@ -417,8 +420,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
segmentIndex = representation.getIndex();
}
- public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation)
- throws BehindLiveWindowException{
+ /* package */ void updateRepresentation(long newPeriodDurationUs,
+ Representation newRepresentation) throws BehindLiveWindowException {
DashSegmentIndex oldIndex = representation.getIndex();
DashSegmentIndex newIndex = newRepresentation.getIndex();
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index 53115a7a0e..2e85f3a1ad 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -690,7 +690,9 @@ public class DashManifestParser extends DefaultHandler
throws XmlPullParserException, IOException {
String schemeIdUri = parseString(xpp, "schemeIdUri", null);
int audioChannels = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri)
- ? parseInt(xpp, "value", Format.NO_VALUE) : Format.NO_VALUE;
+ ? parseInt(xpp, "value", Format.NO_VALUE)
+ : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri)
+ ? parseDolbyChannelConfiguration(xpp) : Format.NO_VALUE);
do {
xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
@@ -901,6 +903,34 @@ public class DashManifestParser extends DefaultHandler
return value == null ? defaultValue : value;
}
+ /**
+ * Parses the number of channels from the value attribute of an AudioElementConfiguration with
+ * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5
+ * in ETSI TS 102 366.
+ *
+ * @param xpp The parser from which to read.
+ * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
+ * not be parsed.
+ */
+ protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) {
+ String value = Util.toLowerInvariant(xpp.getAttributeValue(null, "value"));
+ if (value == null) {
+ return Format.NO_VALUE;
+ }
+ switch (value) {
+ case "4000":
+ return 1;
+ case "a000":
+ return 2;
+ case "f801":
+ return 6;
+ case "fa01":
+ return 8;
+ default:
+ return Format.NO_VALUE;
+ }
+ }
+
private static final class RepresentationInfo {
public final Format format;
diff --git a/library/hls/README.md b/library/hls/README.md
new file mode 100644
index 0000000000..6f7e9d08d9
--- /dev/null
+++ b/library/hls/README.md
@@ -0,0 +1,11 @@
+# ExoPlayer HLS library module #
+
+Provides support for HTTP Live Streaming (HLS) content. To play HLS content,
+instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*`
+ belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/library/hls/build.gradle b/library/hls/build.gradle
index ac77725ca5..5471eacec6 100644
--- a/library/hls/build.gradle
+++ b/library/hls/build.gradle
@@ -35,6 +35,7 @@ android {
dependencies {
compile project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion
+ androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
diff --git a/library/hls/src/androidTest/AndroidManifest.xml b/library/hls/src/androidTest/AndroidManifest.xml
index dcf6c2f940..1abbcad810 100644
--- a/library/hls/src/androidTest/AndroidManifest.xml
+++ b/library/hls/src/androidTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.source.hls.test">
-
+ muxedCaptionFormats, int trackSelectionReason,
Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex,
int discontinuitySequenceNumber, boolean isMasterTimestampSource,
- TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey,
- byte[] encryptionIv) {
- super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
+ TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, String keyFormat,
+ byte[] keyData, byte[] encryptionIv) {
+ super(buildDataSource(dataSource, keyFormat, keyData, encryptionIv), dataSpec, hlsUrl.format,
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.initDataSpec = initDataSpec;
@@ -327,15 +331,16 @@ import java.util.concurrent.atomic.AtomicInteger;
// Internal factory methods.
/**
- * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in
- * order to decrypt the loaded data. Else returns the original.
+ * If the content is encrypted using the "identity" key format, returns an
+ * {@link Aes128DataSource} that wraps the original in order to decrypt the loaded data. Else
+ * returns the original.
*/
- private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey,
+ private static DataSource buildDataSource(DataSource dataSource, String keyFormat, byte[] keyData,
byte[] encryptionIv) {
- if (encryptionKey == null || encryptionIv == null) {
- return dataSource;
+ if (HlsMediaPlaylist.KEYFORMAT_IDENTITY.equals(keyFormat)) {
+ return new Aes128DataSource(dataSource, keyData, encryptionIv);
}
- return new Aes128DataSource(dataSource, encryptionKey, encryptionIv);
+ return dataSource;
}
private Extractor createExtractor() {
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
index 450644f60f..e423a682f3 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
@@ -50,8 +50,8 @@ import java.io.IOException;
}
@Override
- public void skipData(long positionUs) {
- sampleStreamWrapper.skipData(group, positionUs);
+ public int skipData(long positionUs) {
+ return sampleStreamWrapper.skipData(group, positionUs);
}
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
index 0b6d1863bd..00a3cd4a85 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -229,7 +229,7 @@ import java.util.LinkedList;
// sample queue, or if we haven't read anything from the queue since the previous seek
// (this case is common for sparse tracks such as metadata tracks). In all other cases a
// seek is required.
- seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
+ seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
&& sampleQueue.getReadIndex() != 0;
}
}
@@ -320,6 +320,7 @@ import java.util.LinkedList;
return true;
}
+ @Override
public long getBufferedPositionUs() {
if (loadingFinished) {
return C.TIME_END_OF_SOURCE;
@@ -402,12 +403,13 @@ import java.util.LinkedList;
lastSeekPositionUs);
}
- /* package */ void skipData(int trackGroupIndex, long positionUs) {
+ /* package */ int skipData(int trackGroupIndex, long positionUs) {
SampleQueue sampleQueue = sampleQueues[trackGroupIndex];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.advanceToEnd();
+ return sampleQueue.advanceToEnd();
} else {
- sampleQueue.advanceTo(positionUs, true, true);
+ int skipCount = sampleQueue.advanceTo(positionUs, true, true);
+ return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
}
}
@@ -760,7 +762,8 @@ import java.util.LinkedList;
for (int i = 0; i < trackCount; i++) {
SampleQueue sampleQueue = sampleQueues[i];
sampleQueue.rewind();
- boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false);
+ boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)
+ != SampleQueue.ADVANCE_FAILED;
// If we have AV tracks then an in-queue seek is successful if the seek into every AV queue
// is successful. We ignore whether seeks within non-AV queues are successful in this case, as
// they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
index b38763f7e8..04192def9d 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -108,6 +109,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
? Collections.unmodifiableList(muxedCaptionFormats) : null;
}
+ /**
+ * Returns a copy of this playlist which includes only the renditions identified by the given
+ * urls.
+ *
+ * @param renditionUrls List of rendition urls.
+ * @return A copy of this playlist which includes only the renditions identified by the given
+ * urls.
+ */
+ public HlsMasterPlaylist copy(List renditionUrls) {
+ return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls),
+ copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls),
+ muxedAudioFormat, muxedCaptionFormats);
+ }
+
/**
* Creates a playlist with a single variant.
*
@@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
emptyList, null, null);
}
+ private static List copyRenditionsList(List renditions, List urls) {
+ List copiedRenditions = new ArrayList<>(urls.size());
+ for (int i = 0; i < renditions.size(); i++) {
+ HlsUrl rendition = renditions.get(i);
+ if (urls.contains(rendition.url)) {
+ copiedRenditions.add(rendition);
+ }
+ }
+ return copiedRenditions;
+ }
+
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
index db4f041be2..1b573f41c2 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -53,6 +53,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* Whether the segment is encrypted, as defined by #EXT-X-KEY.
*/
public final boolean isEncrypted;
+ /**
+ * The key format as defined by #EXT-X-KEY, or null if the segment is not encrypted.
+ */
+ public final String keyFormat;
/**
* The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted.
*/
@@ -73,7 +77,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final long byterangeLength;
public Segment(String uri, long byterangeOffset, long byterangeLength) {
- this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
+ this(uri, 0, -1, C.TIME_UNSET, false, null, null, null, byterangeOffset, byterangeLength);
}
/**
@@ -82,19 +86,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.
* @param relativeStartTimeUs See {@link #relativeStartTimeUs}.
* @param isEncrypted See {@link #isEncrypted}.
+ * @param keyFormat See {@link #keyFormat}.
* @param encryptionKeyUri See {@link #encryptionKeyUri}.
* @param encryptionIV See {@link #encryptionIV}.
* @param byterangeOffset See {@link #byterangeOffset}.
* @param byterangeLength See {@link #byterangeLength}.
*/
public Segment(String url, long durationUs, int relativeDiscontinuitySequence,
- long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
- long byterangeOffset, long byterangeLength) {
+ long relativeStartTimeUs, boolean isEncrypted, String keyFormat, String encryptionKeyUri,
+ String encryptionIV, long byterangeOffset, long byterangeLength) {
this.url = url;
this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs;
this.isEncrypted = isEncrypted;
+ this.keyFormat = keyFormat;
this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset;
@@ -110,7 +116,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
/**
- * Type of the playlist as defined by #EXT-X-PLAYLIST-TYPE.
+ * The identity key format, as defined by #EXT-X-KEY.
+ */
+ public static final String KEYFORMAT_IDENTITY = "identity";
+
+ /**
+ * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT})
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
index 09d6fcfa18..dc5fd96f35 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -69,7 +69,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser
-
+ keys = Arrays.asList(
+ new TrackKey(0, 0),
+ new TrackKey(0, 2),
+ new TrackKey(1, 0));
+ // Keys don't need to be in any particular order
+ Collections.shuffle(keys, new Random(0));
+
+ SsManifest copyManifest = sourceManifest.copy(keys);
+
+ SsManifest expectedManifest = newSsManifest(
+ newStreamElement("1", formats[0][0], formats[0][2]),
+ newStreamElement("2", formats[1][0]));
+ assertManifestEquals(expectedManifest, copyManifest);
+ }
+
+ public void testCopyRemoveStreamElement() throws Exception {
+ Format[][] formats = newFormats(2, 3);
+ SsManifest sourceManifest = newSsManifest(
+ newStreamElement("1", formats[0]),
+ newStreamElement("2", formats[1]));
+
+ List keys = Arrays.asList(
+ new TrackKey(1, 0));
+ // Keys don't need to be in any particular order
+ Collections.shuffle(keys, new Random(0));
+
+ SsManifest copyManifest = sourceManifest.copy(keys);
+
+ SsManifest expectedManifest = newSsManifest(
+ newStreamElement("2", formats[1][0]));
+ assertManifestEquals(expectedManifest, copyManifest);
+ }
+
+ private static void assertManifestEquals(SsManifest expected, SsManifest actual) {
+ assertEquals(expected.durationUs, actual.durationUs);
+ assertEquals(expected.dvrWindowLengthUs, actual.dvrWindowLengthUs);
+ assertEquals(expected.isLive, actual.isLive);
+ assertEquals(expected.lookAheadCount, actual.lookAheadCount);
+ assertEquals(expected.majorVersion, actual.majorVersion);
+ assertEquals(expected.minorVersion, actual.minorVersion);
+ assertEquals(expected.protectionElement.uuid, actual.protectionElement.uuid);
+ assertEquals(expected.protectionElement, actual.protectionElement);
+ for (int i = 0; i < expected.streamElements.length; i++) {
+ StreamElement expectedStreamElement = expected.streamElements[i];
+ StreamElement actualStreamElement = actual.streamElements[i];
+ assertEquals(expectedStreamElement.chunkCount, actualStreamElement.chunkCount);
+ assertEquals(expectedStreamElement.displayHeight, actualStreamElement.displayHeight);
+ assertEquals(expectedStreamElement.displayWidth, actualStreamElement.displayWidth);
+ assertEquals(expectedStreamElement.language, actualStreamElement.language);
+ assertEquals(expectedStreamElement.maxHeight, actualStreamElement.maxHeight);
+ assertEquals(expectedStreamElement.maxWidth, actualStreamElement.maxWidth);
+ assertEquals(expectedStreamElement.name, actualStreamElement.name);
+ assertEquals(expectedStreamElement.subType, actualStreamElement.subType);
+ assertEquals(expectedStreamElement.timescale, actualStreamElement.timescale);
+ assertEquals(expectedStreamElement.type, actualStreamElement.type);
+ MoreAsserts.assertEquals(expectedStreamElement.formats, actualStreamElement.formats);
+ }
+ }
+
+ private static Format[][] newFormats(int streamElementCount, int trackCounts) {
+ Format[][] formats = new Format[streamElementCount][];
+ for (int i = 0; i < streamElementCount; i++) {
+ formats[i] = new Format[trackCounts];
+ for (int j = 0; j < trackCounts; j++) {
+ formats[i][j] = newFormat(i + "." + j);
+ }
+ }
+ return formats;
+ }
+
+ private static SsManifest newSsManifest(StreamElement... streamElements) {
+ return new SsManifest(1, 2, 1000, 5000, 0, 0, false, DUMMY_PROTECTION_ELEMENT, streamElements);
+ }
+
+ private static StreamElement newStreamElement(String name, Format... formats) {
+ return new StreamElement("baseUri", "chunkTemplate", C.TRACK_TYPE_VIDEO, "subType",
+ 1000, name, 1024, 768, 1024, 768, null, formats, Collections.emptyList(), 0);
+ }
+
+ private static Format newFormat(String id) {
+ return Format.createContainerFormat(id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
+ Format.NO_VALUE, 0, null);
+ }
+
+}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
index 1bb877eb59..fbc3726a0e 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
@@ -21,6 +21,9 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -96,16 +99,60 @@ public class SsManifest {
public SsManifest(int majorVersion, int minorVersion, long timescale, long duration,
long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
StreamElement[] streamElements) {
+ this(majorVersion, minorVersion,
+ duration == 0 ? C.TIME_UNSET
+ : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale),
+ dvrWindowLength == 0 ? C.TIME_UNSET
+ : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale),
+ lookAheadCount, isLive, protectionElement, streamElements);
+ }
+
+ private SsManifest(int majorVersion, int minorVersion, long durationUs, long dvrWindowLengthUs,
+ int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
+ StreamElement[] streamElements) {
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
+ this.durationUs = durationUs;
+ this.dvrWindowLengthUs = dvrWindowLengthUs;
this.lookAheadCount = lookAheadCount;
this.isLive = isLive;
this.protectionElement = protectionElement;
this.streamElements = streamElements;
- dvrWindowLengthUs = dvrWindowLength == 0 ? C.TIME_UNSET
- : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale);
- durationUs = duration == 0 ? C.TIME_UNSET
- : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale);
+ }
+
+ /**
+ * Creates a copy of this manifest which includes only the tracks identified by the given keys.
+ *
+ * @param trackKeys List of keys for the tracks to be included in the copy.
+ * @return A copy of this manifest with the selected tracks.
+ * @throws IndexOutOfBoundsException If a key has an invalid index.
+ */
+ public final SsManifest copy(List trackKeys) {
+ LinkedList sortedKeys = new LinkedList<>(trackKeys);
+ Collections.sort(sortedKeys);
+
+ StreamElement currentStreamElement = null;
+ List copiedStreamElements = new ArrayList<>();
+ List copiedFormats = new ArrayList<>();
+ for (int i = 0; i < sortedKeys.size(); i++) {
+ TrackKey key = sortedKeys.get(i);
+ StreamElement streamElement = streamElements[key.streamElementIndex];
+ if (streamElement != currentStreamElement && currentStreamElement != null) {
+ // We're advancing to a new stream element. Add the current one.
+ copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0])));
+ copiedFormats.clear();
+ }
+ currentStreamElement = streamElement;
+ copiedFormats.add(streamElement.formats[key.trackIndex]);
+ }
+ if (currentStreamElement != null) {
+ // Add the last stream element.
+ copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0])));
+ }
+
+ StreamElement[] copiedStreamElementsArray = copiedStreamElements.toArray(new StreamElement[0]);
+ return new SsManifest(majorVersion, minorVersion, durationUs, dvrWindowLengthUs, lookAheadCount,
+ isLive, protectionElement, copiedStreamElementsArray);
}
/**
@@ -156,6 +203,16 @@ public class SsManifest {
long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
int displayHeight, String language, Format[] formats, List chunkStartTimes,
long lastChunkDuration) {
+ this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight,
+ displayWidth, displayHeight, language, formats, chunkStartTimes,
+ Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale),
+ Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale));
+ }
+
+ private StreamElement(String baseUri, String chunkTemplate, int type, String subType,
+ long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
+ int displayHeight, String language, Format[] formats, List chunkStartTimes,
+ long[] chunkStartTimesUs, long lastChunkDurationUs) {
this.baseUri = baseUri;
this.chunkTemplate = chunkTemplate;
this.type = type;
@@ -168,12 +225,23 @@ public class SsManifest {
this.displayHeight = displayHeight;
this.language = language;
this.formats = formats;
- this.chunkCount = chunkStartTimes.size();
this.chunkStartTimes = chunkStartTimes;
- lastChunkDurationUs =
- Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale);
- chunkStartTimesUs =
- Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale);
+ this.chunkStartTimesUs = chunkStartTimesUs;
+ this.lastChunkDurationUs = lastChunkDurationUs;
+ chunkCount = chunkStartTimes.size();
+ }
+
+ /**
+ * Creates a copy of this stream element with the formats replaced with those specified.
+ *
+ * @param formats The formats to be included in the copy.
+ * @return A copy of this stream element with the formats replaced.
+ * @throws IndexOutOfBoundsException If a key has an invalid index.
+ */
+ public StreamElement copy(Format[] formats) {
+ return new StreamElement(baseUri, chunkTemplate, type, subType, timescale, name, maxWidth,
+ maxHeight, displayWidth, displayHeight, language, formats, chunkStartTimes,
+ chunkStartTimesUs, lastChunkDurationUs);
}
/**
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java
new file mode 100644
index 0000000000..ed52e6fa12
--- /dev/null
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java
@@ -0,0 +1,76 @@
+/*
+ * 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.source.smoothstreaming.manifest;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+/**
+ * Uniquely identifies a track in a {@link SsManifest}.
+ */
+public final class TrackKey implements Parcelable, Comparable {
+
+ public final int streamElementIndex;
+ public final int trackIndex;
+
+ public TrackKey(int streamElementIndex, int trackIndex) {
+ this.streamElementIndex = streamElementIndex;
+ this.trackIndex = trackIndex;
+ }
+
+ @Override
+ public String toString() {
+ return streamElementIndex + "." + trackIndex;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(streamElementIndex);
+ dest.writeInt(trackIndex);
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public TrackKey createFromParcel(Parcel in) {
+ return new TrackKey(in.readInt(), in.readInt());
+ }
+
+ @Override
+ public TrackKey[] newArray(int size) {
+ return new TrackKey[size];
+ }
+ };
+
+ // Comparable implementation.
+
+ @Override
+ public int compareTo(@NonNull TrackKey o) {
+ int result = streamElementIndex - o.streamElementIndex;
+ if (result == 0) {
+ result = trackIndex - o.trackIndex;
+ }
+ return result;
+ }
+
+}
diff --git a/library/ui/README.md b/library/ui/README.md
new file mode 100644
index 0000000000..34e93e43af
--- /dev/null
+++ b/library/ui/README.md
@@ -0,0 +1,10 @@
+# ExoPlayer UI library module #
+
+Provides UI components and resources for use with ExoPlayer.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*`
+ belong to this module.
+
+[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java
index cb5e3465f8..be04ce2fe0 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java
@@ -93,6 +93,11 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener
// Do nothing.
}
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ // Do nothing.
+ }
+
@Override
public void onPositionDiscontinuity() {
updateAndPost();
@@ -186,8 +191,9 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener
return "";
}
counters.ensureUpdated();
- return " rb:" + counters.renderedOutputBufferCount
+ return " sib:" + counters.skippedInputBufferCount
+ " sb:" + counters.skippedOutputBufferCount
+ + " rb:" + counters.renderedOutputBufferCount
+ " db:" + counters.droppedOutputBufferCount
+ " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount;
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java
index 523c7fd73d..8fe8dbfa5d 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java
@@ -378,7 +378,6 @@ public class DefaultTimeBar extends View implements TimeBar {
super.onSizeChanged(width, height, oldWidth, oldHeight);
}
- @TargetApi(14)
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
index 6ddbfed973..3e6b5bf158 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
@@ -16,12 +16,12 @@
package com.google.android.exoplayer2.ui;
import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -312,6 +312,8 @@ public class PlaybackControlView extends FrameLayout {
private long hideAtMs;
private long[] adGroupTimesMs;
private boolean[] playedAdGroups;
+ private long[] extraAdGroupTimesMs;
+ private boolean[] extraPlayedAdGroups;
private final Runnable updateProgressAction = new Runnable() {
@Override
@@ -364,6 +366,8 @@ public class PlaybackControlView extends FrameLayout {
formatter = new Formatter(formatBuilder, Locale.getDefault());
adGroupTimesMs = new long[0];
playedAdGroups = new boolean[0];
+ extraAdGroupTimesMs = new long[0];
+ extraPlayedAdGroups = new boolean[0];
componentListener = new ComponentListener();
controlDispatcher = DEFAULT_CONTROL_DISPATCHER;
@@ -462,6 +466,29 @@ public class PlaybackControlView extends FrameLayout {
updateTimeBarMode();
}
+ /**
+ * Sets the millisecond positions of extra ad markers relative to the start of the window (or
+ * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
+ * markers are shown in addition to any ad markers for ads in the player's timeline.
+ *
+ * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or
+ * {@code null} to show no extra ad markers.
+ * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad
+ * markers.
+ */
+ public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs,
+ @Nullable boolean[] extraPlayedAdGroups) {
+ if (extraAdGroupTimesMs == null) {
+ this.extraAdGroupTimesMs = new long[0];
+ this.extraPlayedAdGroups = new boolean[0];
+ } else {
+ Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length);
+ this.extraAdGroupTimesMs = extraAdGroupTimesMs;
+ this.extraPlayedAdGroups = extraPlayedAdGroups;
+ }
+ updateProgress();
+ }
+
/**
* Sets the {@link VisibilityListener}.
*
@@ -647,9 +674,10 @@ public class PlaybackControlView extends FrameLayout {
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
isSeekable = window.isSeekable;
- enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode())
- || isSeekable || !window.isDynamic;
- enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic;
+ enablePrevious = isSeekable || !window.isDynamic
+ || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET;
+ enableNext = window.isDynamic
+ || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET;
if (player.isPlayingAd()) {
// Always hide player controls during ads.
hide();
@@ -768,7 +796,15 @@ public class PlaybackControlView extends FrameLayout {
bufferedPosition += player.getBufferedPosition();
}
if (timeBar != null) {
- timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, adGroupCount);
+ int extraAdGroupCount = extraAdGroupTimesMs.length;
+ int totalAdGroupCount = adGroupCount + extraAdGroupCount;
+ if (totalAdGroupCount > adGroupTimesMs.length) {
+ adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount);
+ playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount);
+ }
+ System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount);
+ System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount);
+ timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount);
}
}
if (durationView != null) {
@@ -814,17 +850,8 @@ public class PlaybackControlView extends FrameLayout {
return;
}
view.setEnabled(enabled);
- if (Util.SDK_INT >= 11) {
- setViewAlphaV11(view, enabled ? 1f : 0.3f);
- view.setVisibility(VISIBLE);
- } else {
- view.setVisibility(enabled ? VISIBLE : INVISIBLE);
- }
- }
-
- @TargetApi(11)
- private void setViewAlphaV11(View view, float alpha) {
- view.setAlpha(alpha);
+ view.setAlpha(enabled ? 1f : 0.3f);
+ view.setVisibility(VISIBLE);
}
private void previous() {
@@ -1052,6 +1079,11 @@ public class PlaybackControlView extends FrameLayout {
updateNavigation();
}
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ // TODO: Update UI.
+ }
+
@Override
public void onPositionDiscontinuity() {
updateNavigation();
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
index 2bba9071fd..bdbdf34331 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
@@ -43,7 +43,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
-import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
@@ -379,9 +379,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
/**
- * Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
- * {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
- * assignments are overridden.
+ * Set the {@link SimpleExoPlayer} to use.
*
* To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to
* use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} rather
@@ -397,8 +395,8 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
if (this.player != null) {
this.player.removeListener(componentListener);
- this.player.clearTextOutput(componentListener);
- this.player.clearVideoListener(componentListener);
+ this.player.removeTextOutput(componentListener);
+ this.player.removeVideoListener(componentListener);
if (surfaceView instanceof TextureView) {
this.player.clearVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SurfaceView) {
@@ -418,8 +416,8 @@ public final class SimpleExoPlayerView extends FrameLayout {
} else if (surfaceView instanceof SurfaceView) {
player.setVideoSurfaceView((SurfaceView) surfaceView);
}
- player.setVideoListener(componentListener);
- player.setTextOutput(componentListener);
+ player.addVideoListener(componentListener);
+ player.addTextOutput(componentListener);
player.addListener(componentListener);
maybeShowController(false);
updateForCurrentTrackSelections();
@@ -668,10 +666,15 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
/**
- * Gets the view onto which video is rendered. This is either a {@link SurfaceView} (default)
- * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
+ * Gets the view onto which video is rendered. This is a:
+ *
+ *
{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to
+ * {@code surface_view}.
+ *
{@link TextureView} if {@code surface_type} is {@code texture_view}.
+ *
{@code null} if {@code surface_type} is {@code none}.
+ *
*
- * @return Either a {@link SurfaceView} or a {@link TextureView}.
+ * @return The {@link SurfaceView}, {@link TextureView} or {@code null}.
*/
public View getVideoSurfaceView() {
return surfaceView;
@@ -841,10 +844,10 @@ public final class SimpleExoPlayerView extends FrameLayout {
aspectRatioFrame.setResizeMode(resizeMode);
}
- private final class ComponentListener implements SimpleExoPlayer.VideoListener,
- TextRenderer.Output, Player.EventListener {
+ private final class ComponentListener implements TextOutput, SimpleExoPlayer.VideoListener,
+ Player.EventListener {
- // TextRenderer.Output implementation
+ // TextOutput implementation
@Override
public void onCues(List cues) {
@@ -853,7 +856,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
}
- // SimpleExoPlayer.VideoListener implementation
+ // SimpleExoPlayer.VideoInfoListener implementation
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
@@ -893,6 +896,11 @@ public final class SimpleExoPlayerView extends FrameLayout {
// Do nothing.
}
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ // Do nothing.
+ }
+
@Override
public void onPlayerError(ExoPlaybackException e) {
// Do nothing.
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
index 3bcfcc3ef3..618f2fa336 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
@@ -25,7 +25,7 @@ import android.view.View;
import android.view.accessibility.CaptioningManager;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
-import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +33,7 @@ import java.util.List;
/**
* A view for displaying subtitle {@link Cue}s.
*/
-public final class SubtitleView extends View implements TextRenderer.Output {
+public final class SubtitleView extends View implements TextOutput {
/**
* The default fractional text size.
diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml
new file mode 100644
index 0000000000..28ac6a5786
--- /dev/null
+++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png
new file mode 100644
index 0000000000..52a805aac1
Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png differ
diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png
new file mode 100644
index 0000000000..80ec43a119
Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png differ
diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png
new file mode 100644
index 0000000000..0924b2cb69
Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png differ
diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png
new file mode 100644
index 0000000000..ede80c9341
Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png differ
diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png
new file mode 100644
index 0000000000..4c5e141a3f
Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png differ
diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml
index 103877f1e6..2510552c0c 100644
--- a/library/ui/src/main/res/values-af/strings.xml
+++ b/library/ui/src/main/res/values-af/strings.xml
@@ -25,4 +25,5 @@
"Herhaal alles""Herhaal niks""Herhaal een"
+ "Skommel"
diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml
index 356566cb87..165b5eee62 100644
--- a/library/ui/src/main/res/values-am/strings.xml
+++ b/library/ui/src/main/res/values-am/strings.xml
@@ -25,4 +25,5 @@
"ሁሉንም ድገም""ምንም አትድገም""አንዱን ድገም"
+ "በው"
diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml
index 4bdbda061c..239f01be6b 100644
--- a/library/ui/src/main/res/values-ar/strings.xml
+++ b/library/ui/src/main/res/values-ar/strings.xml
@@ -25,4 +25,5 @@
"تكرار الكل""عدم التكرار""تكرار مقطع واحد"
+ "ترتيب عشوائي"
diff --git a/library/ui/src/main/res/values-az-rAZ/strings.xml b/library/ui/src/main/res/values-az-rAZ/strings.xml
index 771335952f..1071cd5542 100644
--- a/library/ui/src/main/res/values-az-rAZ/strings.xml
+++ b/library/ui/src/main/res/values-az-rAZ/strings.xml
@@ -25,4 +25,5 @@
"Bütün təkrarlayın""Təkrar bir""Heç bir təkrar"
+ "Qarışdır"
diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml
index 7c373b5b55..a9d35e5cb6 100644
--- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml
+++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml
@@ -25,4 +25,5 @@
"Ponovi sve""Ne ponavljaj nijednu""Ponovi jednu"
+ "Pusti nasumično"
diff --git a/library/ui/src/main/res/values-be-rBY/strings.xml b/library/ui/src/main/res/values-be-rBY/strings.xml
index 7790a7887f..69b24ad5e9 100644
--- a/library/ui/src/main/res/values-be-rBY/strings.xml
+++ b/library/ui/src/main/res/values-be-rBY/strings.xml
@@ -25,4 +25,5 @@
"Паўтарыць усё""Паўтараць ні""Паўтарыць адзін"
+ "Перамяшаць"
diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml
index ce9e3d6943..e350479788 100644
--- a/library/ui/src/main/res/values-bg/strings.xml
+++ b/library/ui/src/main/res/values-bg/strings.xml
@@ -25,4 +25,5 @@
"Повтаряне на всички""Без повтаряне""Повтаряне на един елемент"
+ "Разбъркване"
diff --git a/library/ui/src/main/res/values-bn-rBD/strings.xml b/library/ui/src/main/res/values-bn-rBD/strings.xml
index 5f8ebfa98e..446ef982a3 100644
--- a/library/ui/src/main/res/values-bn-rBD/strings.xml
+++ b/library/ui/src/main/res/values-bn-rBD/strings.xml
@@ -25,4 +25,5 @@
"সবগুলির পুনরাবৃত্তি করুন""একটিরও পুনরাবৃত্তি করবেন না""একটির পুনরাবৃত্তি করুন"
+ "অদলবদল"
diff --git a/library/ui/src/main/res/values-bs-rBA/strings.xml b/library/ui/src/main/res/values-bs-rBA/strings.xml
index ef47099760..186b1058d9 100644
--- a/library/ui/src/main/res/values-bs-rBA/strings.xml
+++ b/library/ui/src/main/res/values-bs-rBA/strings.xml
@@ -25,4 +25,5 @@
"Ponovite sve""Ne ponavljaju""Ponovite jedan"
+ "Izmiješaj"
diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml
index a42fe3b9cb..fd76a8e08e 100644
--- a/library/ui/src/main/res/values-ca/strings.xml
+++ b/library/ui/src/main/res/values-ca/strings.xml
@@ -25,4 +25,5 @@
"Repeteix-ho tot""No en repeteixis cap""Repeteix-ne un"
+ "Reprodueix aleatòriament"
diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml
index 9c1e50ce27..087ab79c25 100644
--- a/library/ui/src/main/res/values-cs/strings.xml
+++ b/library/ui/src/main/res/values-cs/strings.xml
@@ -25,4 +25,5 @@
"Opakovat vše""Neopakovat""Opakovat jednu položku"
+ "Náhodně"
diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml
index 3ec132ebb7..0ae23ee288 100644
--- a/library/ui/src/main/res/values-da/strings.xml
+++ b/library/ui/src/main/res/values-da/strings.xml
@@ -25,4 +25,5 @@
"Gentag alle""Gentag ingen""Gentag en"
+ "Bland"
diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml
index a1dc749864..37ca6c44ac 100644
--- a/library/ui/src/main/res/values-de/strings.xml
+++ b/library/ui/src/main/res/values-de/strings.xml
@@ -25,4 +25,5 @@
"Alle wiederholen""Keinen Titel wiederholen""Einen Titel wiederholen"
+ "Zufallsmix"
diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml
index 845011fe55..534192e185 100644
--- a/library/ui/src/main/res/values-el/strings.xml
+++ b/library/ui/src/main/res/values-el/strings.xml
@@ -25,4 +25,5 @@
"Επανάληψη όλων""Καμία επανάληψη""Επανάληψη ενός στοιχείου"
+ "Τυχαία αναπαραγωγή"
diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml
index 8a1742c8ca..0b4c465853 100644
--- a/library/ui/src/main/res/values-en-rAU/strings.xml
+++ b/library/ui/src/main/res/values-en-rAU/strings.xml
@@ -25,4 +25,5 @@
"Repeat all""Repeat none""Repeat one"
+ "Shuffle"
diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml
index 8a1742c8ca..0b4c465853 100644
--- a/library/ui/src/main/res/values-en-rGB/strings.xml
+++ b/library/ui/src/main/res/values-en-rGB/strings.xml
@@ -25,4 +25,5 @@
"Repeat all""Repeat none""Repeat one"
+ "Shuffle"
diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml
index 8a1742c8ca..0b4c465853 100644
--- a/library/ui/src/main/res/values-en-rIN/strings.xml
+++ b/library/ui/src/main/res/values-en-rIN/strings.xml
@@ -25,4 +25,5 @@
"Repeat all""Repeat none""Repeat one"
+ "Shuffle"
diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml
index f2ec848fb6..e6cf3fc6f2 100644
--- a/library/ui/src/main/res/values-es-rUS/strings.xml
+++ b/library/ui/src/main/res/values-es-rUS/strings.xml
@@ -25,4 +25,5 @@
"Repetir todo""No repetir""Repetir uno"
+ "Reproducir aleatoriamente"
diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml
index 116f064223..04e1ea038c 100644
--- a/library/ui/src/main/res/values-es/strings.xml
+++ b/library/ui/src/main/res/values-es/strings.xml
@@ -25,4 +25,5 @@
"Repetir todo""No repetir""Repetir uno"
+ "Reproducción aleatoria"
diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/library/ui/src/main/res/values-et-rEE/strings.xml
index 153611ece4..004ec7e6c3 100644
--- a/library/ui/src/main/res/values-et-rEE/strings.xml
+++ b/library/ui/src/main/res/values-et-rEE/strings.xml
@@ -25,4 +25,5 @@
"Korda kõike""Ära korda midagi""Korda ühte"
+ "Esita juhuslikus järjekorras"
diff --git a/library/ui/src/main/res/values-eu-rES/strings.xml b/library/ui/src/main/res/values-eu-rES/strings.xml
index 1128572d9a..6a3345303a 100644
--- a/library/ui/src/main/res/values-eu-rES/strings.xml
+++ b/library/ui/src/main/res/values-eu-rES/strings.xml
@@ -25,4 +25,5 @@
"Errepikatu guztiak""Ez errepikatu""Errepikatu bat"
+ "Erreproduzitu ausaz"
diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml
index d6be77323b..101fcdbfb5 100644
--- a/library/ui/src/main/res/values-fa/strings.xml
+++ b/library/ui/src/main/res/values-fa/strings.xml
@@ -25,4 +25,5 @@
"تکرار همه""تکرار هیچکدام""یکبار تکرار"
+ "پخش درهم"
diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml
index 10e4b0bbe3..92feb86683 100644
--- a/library/ui/src/main/res/values-fi/strings.xml
+++ b/library/ui/src/main/res/values-fi/strings.xml
@@ -25,4 +25,5 @@
"Toista kaikki""Toista ei mitään""Toista yksi"
+ "Toista satunnaisesti"
diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml
index d8852b5d3f..45fc0a86f9 100644
--- a/library/ui/src/main/res/values-fr-rCA/strings.xml
+++ b/library/ui/src/main/res/values-fr-rCA/strings.xml
@@ -25,4 +25,5 @@
"Tout lire en boucle""Aucune répétition""Répéter un élément"
+ "Lecture aléatoire"
diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml
index acf3670fa4..82b5a40626 100644
--- a/library/ui/src/main/res/values-fr/strings.xml
+++ b/library/ui/src/main/res/values-fr/strings.xml
@@ -25,4 +25,5 @@
"Tout lire en boucle""Ne rien lire en boucle""Lire en boucle un élément"
+ "Lire en mode aléatoire"
diff --git a/library/ui/src/main/res/values-gl-rES/strings.xml b/library/ui/src/main/res/values-gl-rES/strings.xml
index 81b854cafe..7062d8d023 100644
--- a/library/ui/src/main/res/values-gl-rES/strings.xml
+++ b/library/ui/src/main/res/values-gl-rES/strings.xml
@@ -25,4 +25,5 @@
"Repetir todo""Non repetir""Repetir un"
+ "Reprodución aleatoria"
diff --git a/library/ui/src/main/res/values-gu-rIN/strings.xml b/library/ui/src/main/res/values-gu-rIN/strings.xml
index 6d51c29f97..ed78b1ee30 100644
--- a/library/ui/src/main/res/values-gu-rIN/strings.xml
+++ b/library/ui/src/main/res/values-gu-rIN/strings.xml
@@ -25,4 +25,5 @@
"બધા પુનરાવર્તન કરો""કંઈ પુનરાવર્તન કરો""એક પુનરાવર્તન કરો"
+ "શફલ કરો"
diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml
index eadb0519df..ec624b1f35 100644
--- a/library/ui/src/main/res/values-hi/strings.xml
+++ b/library/ui/src/main/res/values-hi/strings.xml
@@ -25,4 +25,5 @@
"सभी को दोहराएं""कुछ भी न दोहराएं""एक दोहराएं"
+ "शफ़ल करें"
diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml
index cb49965640..7cb23e11dd 100644
--- a/library/ui/src/main/res/values-hr/strings.xml
+++ b/library/ui/src/main/res/values-hr/strings.xml
@@ -25,4 +25,5 @@
"Ponovi sve""Bez ponavljanja""Ponovi jedno"
+ "Reproduciraj nasumično"
diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml
index 43ac8f51ff..cf3d34c88f 100644
--- a/library/ui/src/main/res/values-hu/strings.xml
+++ b/library/ui/src/main/res/values-hu/strings.xml
@@ -25,4 +25,5 @@
"Összes ismétlése""Nincs ismétlés""Egy ismétlése"
+ "Véletlenszerű lejátszás"
diff --git a/library/ui/src/main/res/values-hy-rAM/strings.xml b/library/ui/src/main/res/values-hy-rAM/strings.xml
index 3b09f9a507..13a489baf5 100644
--- a/library/ui/src/main/res/values-hy-rAM/strings.xml
+++ b/library/ui/src/main/res/values-hy-rAM/strings.xml
@@ -25,4 +25,5 @@
"կրկնել այն ամենը""Չկրկնել""Կրկնել մեկը"
+ "Խառնել"
diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml
index 928be5945a..09b05815e6 100644
--- a/library/ui/src/main/res/values-in/strings.xml
+++ b/library/ui/src/main/res/values-in/strings.xml
@@ -25,4 +25,5 @@
"Ulangi Semua""Jangan Ulangi""Ulangi Satu"
+ "Acak"
diff --git a/library/ui/src/main/res/values-is-rIS/strings.xml b/library/ui/src/main/res/values-is-rIS/strings.xml
index 75be2aeb17..12c4632cdf 100644
--- a/library/ui/src/main/res/values-is-rIS/strings.xml
+++ b/library/ui/src/main/res/values-is-rIS/strings.xml
@@ -25,4 +25,5 @@
"Endurtaka allt""Endurtaka ekkert""Endurtaka eitt"
+ "Stokka"
diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml
index 59117a6b75..aea20db82e 100644
--- a/library/ui/src/main/res/values-it/strings.xml
+++ b/library/ui/src/main/res/values-it/strings.xml
@@ -25,4 +25,5 @@
"Ripeti tutti""Non ripetere nessuno""Ripeti uno"
+ "Riproduci casualmente"
diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml
index 347b137cf2..dd973af50b 100644
--- a/library/ui/src/main/res/values-iw/strings.xml
+++ b/library/ui/src/main/res/values-iw/strings.xml
@@ -25,4 +25,5 @@
"חזור על הכל""אל תחזור על כלום""חזור על פריט אחד"
+ "ערבב"
diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml
index cf2cc49b67..d6ce751d5c 100644
--- a/library/ui/src/main/res/values-ja/strings.xml
+++ b/library/ui/src/main/res/values-ja/strings.xml
@@ -25,4 +25,5 @@
"全曲を繰り返し""繰り返しなし""1曲を繰り返し"
+ "シャッフル"
diff --git a/library/ui/src/main/res/values-ka-rGE/strings.xml b/library/ui/src/main/res/values-ka-rGE/strings.xml
index 75da8dde18..252e52f151 100644
--- a/library/ui/src/main/res/values-ka-rGE/strings.xml
+++ b/library/ui/src/main/res/values-ka-rGE/strings.xml
@@ -25,4 +25,5 @@
"გამეორება ყველა""გაიმეორეთ არცერთი""გაიმეორეთ ერთი"
+ "არეულად დაკვრა"
diff --git a/library/ui/src/main/res/values-kk-rKZ/strings.xml b/library/ui/src/main/res/values-kk-rKZ/strings.xml
index b1ab22ecf6..43eb3dd030 100644
--- a/library/ui/src/main/res/values-kk-rKZ/strings.xml
+++ b/library/ui/src/main/res/values-kk-rKZ/strings.xml
@@ -25,4 +25,5 @@
"Барлығын қайталау""Ешқайсысын қайталамау""Біреуін қайталау"
+ "Кездейсоқ ретпен ойнату"
diff --git a/library/ui/src/main/res/values-km-rKH/strings.xml b/library/ui/src/main/res/values-km-rKH/strings.xml
index dfd9f7d863..653c9f051d 100644
--- a/library/ui/src/main/res/values-km-rKH/strings.xml
+++ b/library/ui/src/main/res/values-km-rKH/strings.xml
@@ -25,4 +25,5 @@
"ធ្វើម្ដងទៀតទាំងអស់""មិនធ្វើឡើងវិញ""ធ្វើឡើងវិញម្ដង"
+ "ច្របល់"
diff --git a/library/ui/src/main/res/values-kn-rIN/strings.xml b/library/ui/src/main/res/values-kn-rIN/strings.xml
index 868af17a65..7368fc8ad3 100644
--- a/library/ui/src/main/res/values-kn-rIN/strings.xml
+++ b/library/ui/src/main/res/values-kn-rIN/strings.xml
@@ -25,4 +25,5 @@
"ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ""ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ""ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ"
+ "ಬೆರೆಸು"
diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml
index 89636ac8a0..99d4a2f9a4 100644
--- a/library/ui/src/main/res/values-ko/strings.xml
+++ b/library/ui/src/main/res/values-ko/strings.xml
@@ -25,4 +25,5 @@
"전체 반복""반복 안함""한 항목 반복"
+ "셔플"
diff --git a/library/ui/src/main/res/values-ky-rKG/strings.xml b/library/ui/src/main/res/values-ky-rKG/strings.xml
index 15fd50468a..9b903a124e 100644
--- a/library/ui/src/main/res/values-ky-rKG/strings.xml
+++ b/library/ui/src/main/res/values-ky-rKG/strings.xml
@@ -25,4 +25,5 @@
"Баарын кайталоо""Эч бирин кайталабоо""Бирөөнү кайталоо"
+ "Аралаштыруу"
diff --git a/library/ui/src/main/res/values-lo-rLA/strings.xml b/library/ui/src/main/res/values-lo-rLA/strings.xml
index 405d0c64fe..702cd54396 100644
--- a/library/ui/src/main/res/values-lo-rLA/strings.xml
+++ b/library/ui/src/main/res/values-lo-rLA/strings.xml
@@ -25,4 +25,5 @@
"ຫຼິ້ນຊ້ຳທັງໝົດ""ບໍ່ຫຼິ້ນຊ້ຳ""ຫຼິ້ນຊ້ຳ"
+ "ຫຼີ້ນແບບສຸ່ມ"
diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml
index bd7d4142fc..d6073f42e3 100644
--- a/library/ui/src/main/res/values-lt/strings.xml
+++ b/library/ui/src/main/res/values-lt/strings.xml
@@ -25,4 +25,5 @@
"Kartoti viską""Nekartoti nieko""Kartoti vieną"
+ "Maišyti"
diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml
index c2ebc70cbd..64393d679a 100644
--- a/library/ui/src/main/res/values-lv/strings.xml
+++ b/library/ui/src/main/res/values-lv/strings.xml
@@ -25,4 +25,5 @@
"Atkārtot visu""Neatkārtot nevienu""Atkārtot vienu"
+ "Atskaņot jauktā secībā"
diff --git a/library/ui/src/main/res/values-mk-rMK/strings.xml b/library/ui/src/main/res/values-mk-rMK/strings.xml
index 14ce7111a4..60858df8b1 100644
--- a/library/ui/src/main/res/values-mk-rMK/strings.xml
+++ b/library/ui/src/main/res/values-mk-rMK/strings.xml
@@ -25,4 +25,5 @@
"Повтори ги сите""Не повторувај ниту една""Повтори една"
+ "По случаен избор"
diff --git a/library/ui/src/main/res/values-ml-rIN/strings.xml b/library/ui/src/main/res/values-ml-rIN/strings.xml
index 17fe7a1655..4e5eddb93e 100644
--- a/library/ui/src/main/res/values-ml-rIN/strings.xml
+++ b/library/ui/src/main/res/values-ml-rIN/strings.xml
@@ -25,4 +25,5 @@
"എല്ലാം ആവർത്തിക്കുക""ഒന്നും ആവർത്തിക്കരുത്""ഒന്ന് ആവർത്തിക്കുക"
+ "ഷഫിൾ ചെയ്യുക"
diff --git a/library/ui/src/main/res/values-mn-rMN/strings.xml b/library/ui/src/main/res/values-mn-rMN/strings.xml
index bf9a7e03bf..4ab26a7f62 100644
--- a/library/ui/src/main/res/values-mn-rMN/strings.xml
+++ b/library/ui/src/main/res/values-mn-rMN/strings.xml
@@ -25,4 +25,5 @@
"Бүгдийг давтах""Алийг нь ч давтахгүй""Нэгийг давтах"
+ "Холих"
diff --git a/library/ui/src/main/res/values-mr-rIN/strings.xml b/library/ui/src/main/res/values-mr-rIN/strings.xml
index df4ac9de6b..7869355b59 100644
--- a/library/ui/src/main/res/values-mr-rIN/strings.xml
+++ b/library/ui/src/main/res/values-mr-rIN/strings.xml
@@ -25,4 +25,5 @@
"सर्व पुनरावृत्ती करा""काहीही पुनरावृत्ती करू नका""एक पुनरावृत्ती करा"
+ "शफल करा"
diff --git a/library/ui/src/main/res/values-ms-rMY/strings.xml b/library/ui/src/main/res/values-ms-rMY/strings.xml
index 33dfcb40f0..fdde3de079 100644
--- a/library/ui/src/main/res/values-ms-rMY/strings.xml
+++ b/library/ui/src/main/res/values-ms-rMY/strings.xml
@@ -25,4 +25,5 @@
"Ulang semua""Tiada ulangan""Ulangan"
+ "Rombak"
diff --git a/library/ui/src/main/res/values-my-rMM/strings.xml b/library/ui/src/main/res/values-my-rMM/strings.xml
index b4ea5b1155..3d7918d953 100644
--- a/library/ui/src/main/res/values-my-rMM/strings.xml
+++ b/library/ui/src/main/res/values-my-rMM/strings.xml
@@ -25,4 +25,5 @@
"အားလုံး ထပ်တလဲလဲဖွင့်ရန်""ထပ်တလဲလဲမဖွင့်ရန်""တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်"
+ "မွှေနှောက်ဖွင့်ရန်"
diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml
index 679bf1134c..370c759b84 100644
--- a/library/ui/src/main/res/values-nb/strings.xml
+++ b/library/ui/src/main/res/values-nb/strings.xml
@@ -25,4 +25,5 @@
"Gjenta alle""Ikke gjenta noen""Gjenta én"
+ "Spill av i tilfeldig rekkefølge"
diff --git a/library/ui/src/main/res/values-ne-rNP/strings.xml b/library/ui/src/main/res/values-ne-rNP/strings.xml
index 43730c1880..19f43d0392 100644
--- a/library/ui/src/main/res/values-ne-rNP/strings.xml
+++ b/library/ui/src/main/res/values-ne-rNP/strings.xml
@@ -25,4 +25,5 @@
"सबै दोहोर्याउनुहोस्""कुनै पनि नदोहोर्याउनुहोस्""एउटा दोहोर्याउनुहोस्"
+ "मिसाउनुहोस्"
diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml
index 6383c977fc..a67ab2968c 100644
--- a/library/ui/src/main/res/values-nl/strings.xml
+++ b/library/ui/src/main/res/values-nl/strings.xml
@@ -25,4 +25,5 @@
"Alles herhalen""Niet herhalen""Eén herhalen"
+ "Shuffle"
diff --git a/library/ui/src/main/res/values-pa-rIN/strings.xml b/library/ui/src/main/res/values-pa-rIN/strings.xml
index ddf60b0394..6250b90514 100644
--- a/library/ui/src/main/res/values-pa-rIN/strings.xml
+++ b/library/ui/src/main/res/values-pa-rIN/strings.xml
@@ -25,4 +25,5 @@
"ਸਭ ਨੂੰ ਦੁਹਰਾਓ""ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ""ਇੱਕ ਦੁਹਰਾਓ"
+ "ਸ਼ੱਫਲ"
diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml
index 113c568f85..ff1d77fdd5 100644
--- a/library/ui/src/main/res/values-pl/strings.xml
+++ b/library/ui/src/main/res/values-pl/strings.xml
@@ -25,4 +25,5 @@
"Powtórz wszystkie""Nie powtarzaj""Powtórz jeden"
+ "Odtwarzaj losowo"
diff --git a/library/ui/src/main/res/values-pt-rBR/strings.xml b/library/ui/src/main/res/values-pt-rBR/strings.xml
index 87c54358ba..86a91b0677 100644
--- a/library/ui/src/main/res/values-pt-rBR/strings.xml
+++ b/library/ui/src/main/res/values-pt-rBR/strings.xml
@@ -25,4 +25,5 @@
"Repetir tudo""Não repetir""Repetir um"
+ "Reproduzir aleatoriamente"
diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml
index ca34afec3c..5a7144e36b 100644
--- a/library/ui/src/main/res/values-pt-rPT/strings.xml
+++ b/library/ui/src/main/res/values-pt-rPT/strings.xml
@@ -25,4 +25,5 @@
"Repetir tudo""Não repetir""Repetir um"
+ "Reproduzir aleatoriamente"
diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml
index 2fc3191738..8441e4e1cc 100644
--- a/library/ui/src/main/res/values-pt/strings.xml
+++ b/library/ui/src/main/res/values-pt/strings.xml
@@ -25,4 +25,5 @@
"Repetir tudo""Não repetir""Repetir uma"
+ "Reproduzir aleatoriamente"
diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml
index 0b2ce540f7..6b8644e30a 100644
--- a/library/ui/src/main/res/values-ro/strings.xml
+++ b/library/ui/src/main/res/values-ro/strings.xml
@@ -25,4 +25,5 @@
"Repetați toate""Repetați niciuna""Repetați unul"
+ "Redați aleatoriu"
diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml
index 1d179e028c..51d11d6371 100644
--- a/library/ui/src/main/res/values-ru/strings.xml
+++ b/library/ui/src/main/res/values-ru/strings.xml
@@ -25,4 +25,5 @@
"Повторять все""Не повторять""Повторять один элемент"
+ "Перемешать"
diff --git a/library/ui/src/main/res/values-si-rLK/strings.xml b/library/ui/src/main/res/values-si-rLK/strings.xml
index bc37d98eed..eb8453b156 100644
--- a/library/ui/src/main/res/values-si-rLK/strings.xml
+++ b/library/ui/src/main/res/values-si-rLK/strings.xml
@@ -25,4 +25,5 @@
"සියලු නැවත""කිසිවක් නැවත""නැවත නැවත එක්"
+ "කලවම් කරන්න"
diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml
index a6ea26bdf0..2428dbdcce 100644
--- a/library/ui/src/main/res/values-sk/strings.xml
+++ b/library/ui/src/main/res/values-sk/strings.xml
@@ -25,4 +25,5 @@
"Opakovať všetko""Neopakovať""Opakovať jednu položku"
+ "Náhodne prehrávať"
diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml
index 39813fa385..8ed731b0d3 100644
--- a/library/ui/src/main/res/values-sl/strings.xml
+++ b/library/ui/src/main/res/values-sl/strings.xml
@@ -25,4 +25,5 @@
"Ponovi vse""Ne ponovi""Ponovi eno"
+ "Naključno predvajaj"
diff --git a/library/ui/src/main/res/values-sq-rAL/strings.xml b/library/ui/src/main/res/values-sq-rAL/strings.xml
index 0bdc2e5f84..e2d209e10b 100644
--- a/library/ui/src/main/res/values-sq-rAL/strings.xml
+++ b/library/ui/src/main/res/values-sq-rAL/strings.xml
@@ -25,4 +25,5 @@
"Përsërit të gjithë""Përsëritni asnjë""Përsëritni një"
+ "Përziej"
diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml
index 0d54de5f6a..8e43a03079 100644
--- a/library/ui/src/main/res/values-sr/strings.xml
+++ b/library/ui/src/main/res/values-sr/strings.xml
@@ -25,4 +25,5 @@
"Понови све""Понављање је искључено""Понови једну"
+ "Пусти насумично"
diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml
index 0f7f16f91d..5ff1100632 100644
--- a/library/ui/src/main/res/values-sv/strings.xml
+++ b/library/ui/src/main/res/values-sv/strings.xml
@@ -25,4 +25,5 @@
"Upprepa alla""Upprepa inga""Upprepa en"
+ "Blanda"
diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml
index b48af88659..d1d5978f9c 100644
--- a/library/ui/src/main/res/values-sw/strings.xml
+++ b/library/ui/src/main/res/values-sw/strings.xml
@@ -25,4 +25,5 @@
"Rudia zote""Usirudie Yoyote""Rudia Moja"
+ "Changanya"
diff --git a/library/ui/src/main/res/values-ta-rIN/strings.xml b/library/ui/src/main/res/values-ta-rIN/strings.xml
index 3dd64f52f7..43a925aa2e 100644
--- a/library/ui/src/main/res/values-ta-rIN/strings.xml
+++ b/library/ui/src/main/res/values-ta-rIN/strings.xml
@@ -25,4 +25,5 @@
"அனைத்தையும் மீண்டும் இயக்கு""எதையும் மீண்டும் இயக்காதே""ஒன்றை மட்டும் மீண்டும் இயக்கு"
+ "குலை"
diff --git a/library/ui/src/main/res/values-te-rIN/strings.xml b/library/ui/src/main/res/values-te-rIN/strings.xml
index daf337a931..8541a44553 100644
--- a/library/ui/src/main/res/values-te-rIN/strings.xml
+++ b/library/ui/src/main/res/values-te-rIN/strings.xml
@@ -25,4 +25,5 @@
"అన్నీ పునరావృతం చేయి""ఏదీ పునరావృతం చేయవద్దు""ఒకదాన్ని పునరావృతం చేయి"
+ "షఫుల్ చేయి"
diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml
index ff89b8d5f5..cd97712b67 100644
--- a/library/ui/src/main/res/values-th/strings.xml
+++ b/library/ui/src/main/res/values-th/strings.xml
@@ -25,4 +25,5 @@
"เล่นซ้ำทั้งหมด""ไม่เล่นซ้ำ""เล่นซ้ำรายการเดียว"
+ "สุ่มเพลง"
diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml
index 89cf2ef400..e8cb87acdd 100644
--- a/library/ui/src/main/res/values-tl/strings.xml
+++ b/library/ui/src/main/res/values-tl/strings.xml
@@ -25,4 +25,5 @@
"Ulitin Lahat""Walang Uulitin""Ulitin ang Isa"
+ "I-shuffle"
diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml
index 87dba7204c..cd1bfc5444 100644
--- a/library/ui/src/main/res/values-tr/strings.xml
+++ b/library/ui/src/main/res/values-tr/strings.xml
@@ -25,4 +25,5 @@
"Tümünü Tekrarla""Hiçbirini Tekrarlama""Birini Tekrarla"
+ "Karıştır"
diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml
index 1fdfe2bce5..1b0278ae94 100644
--- a/library/ui/src/main/res/values-uk/strings.xml
+++ b/library/ui/src/main/res/values-uk/strings.xml
@@ -25,4 +25,5 @@
"Повторити все""Не повторювати""Повторити один елемент"
+ "Перемішати"
diff --git a/library/ui/src/main/res/values-ur-rPK/strings.xml b/library/ui/src/main/res/values-ur-rPK/strings.xml
index 956374b26a..f253e56c00 100644
--- a/library/ui/src/main/res/values-ur-rPK/strings.xml
+++ b/library/ui/src/main/res/values-ur-rPK/strings.xml
@@ -25,4 +25,5 @@
"سبھی کو دہرائیں""کسی کو نہ دہرائیں""ایک کو دہرائیں"
+ "شفل کریں"
diff --git a/library/ui/src/main/res/values-uz-rUZ/strings.xml b/library/ui/src/main/res/values-uz-rUZ/strings.xml
index 286d4d01ab..a322690b2d 100644
--- a/library/ui/src/main/res/values-uz-rUZ/strings.xml
+++ b/library/ui/src/main/res/values-uz-rUZ/strings.xml
@@ -25,4 +25,5 @@
"Barchasini takrorlash""Takrorlamaslik""Bir marta takrorlash"
+ "Tasodifiy tartibda"
diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml
index 4dea58d494..cff19eca7e 100644
--- a/library/ui/src/main/res/values-vi/strings.xml
+++ b/library/ui/src/main/res/values-vi/strings.xml
@@ -25,4 +25,5 @@
"Lặp lại tất cả""Không lặp lại""Lặp lại một mục"
+ "Trộn bài"
diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml
index e15d84e777..cf3fe5e88b 100644
--- a/library/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/library/ui/src/main/res/values-zh-rCN/strings.xml
@@ -25,4 +25,5 @@
"重复播放全部""不重复播放""重复播放单个视频"
+ "随机播放"
diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml
index ba793e98a8..78fe4ad995 100644
--- a/library/ui/src/main/res/values-zh-rHK/strings.xml
+++ b/library/ui/src/main/res/values-zh-rHK/strings.xml
@@ -25,4 +25,5 @@
"重複播放所有媒體項目""不重複播放任何媒體項目""重複播放一個媒體項目"
+ "隨機播放"
diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml
index bf3364d5cf..3632742904 100644
--- a/library/ui/src/main/res/values-zh-rTW/strings.xml
+++ b/library/ui/src/main/res/values-zh-rTW/strings.xml
@@ -25,4 +25,5 @@
"重複播放所有媒體項目""不重複播放""重複播放單一媒體項目"
+ "隨機播放"
diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml
index d7bebaaa2a..42dd59c97f 100644
--- a/library/ui/src/main/res/values-zu/strings.xml
+++ b/library/ui/src/main/res/values-zu/strings.xml
@@ -25,4 +25,5 @@
"Phinda konke""Ungaphindi lutho""Phida okukodwa"
+ "Shova"
diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml
index b16b1729da..b90d2329b3 100644
--- a/library/ui/src/main/res/values/ids.xml
+++ b/library/ui/src/main/res/values/ids.xml
@@ -28,6 +28,7 @@
+
diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml
index c5d11eeadb..ee8cd78be7 100644
--- a/library/ui/src/main/res/values/strings.xml
+++ b/library/ui/src/main/res/values/strings.xml
@@ -24,4 +24,5 @@
Repeat noneRepeat oneRepeat all
+ Shuffle
diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml
index a67cffe420..4ef8971ccd 100644
--- a/library/ui/src/main/res/values/styles.xml
+++ b/library/ui/src/main/res/values/styles.xml
@@ -51,4 +51,9 @@
@string/exo_controls_pause_description
+
+
diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml
index 053fe4e61c..1a660591d8 100644
--- a/playbacktests/src/androidTest/AndroidManifest.xml
+++ b/playbacktests/src/androidTest/AndroidManifest.xml
@@ -21,7 +21,7 @@
-
+ allData = fakeDataSet.getAllData();
- String[] uriStrings = new String[allData.size()];
+ Uri[] uris = new Uri[allData.size()];
for (int i = 0; i < allData.size(); i++) {
- uriStrings[i] = allData.get(i).uri;
+ uris[i] = allData.get(i).uri;
}
- assertCachedData(cache, fakeDataSet, uriStrings);
+ assertCachedData(cache, fakeDataSet, uris);
}
/**
@@ -51,30 +51,41 @@ public final class CacheAsserts {
*/
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings)
throws IOException {
+ Uri[] uris = new Uri[uriStrings.length];
+ for (int i = 0; i < uriStrings.length; i++) {
+ uris[i] = Uri.parse(uriStrings[i]);
+ }
+ assertCachedData(cache, fakeDataSet, uris);
+ }
+
+ /**
+ * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}.
+ */
+ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris)
+ throws IOException {
int totalLength = 0;
- for (String uriString : uriStrings) {
- byte[] data = fakeDataSet.getData(uriString).getData();
- assertDataCached(cache, uriString, data);
+ for (Uri uri : uris) {
+ byte[] data = fakeDataSet.getData(uri).getData();
+ assertDataCached(cache, uri, data);
totalLength += data.length;
}
assertEquals(totalLength, cache.getCacheSpace());
}
/** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */
- public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, String... uriStrings)
+ public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris)
throws IOException {
- for (String uriString : uriStrings) {
- assertDataCached(cache, uriString, fakeDataSet.getData(uriString).getData());
+ for (Uri uri : uris) {
+ assertDataCached(cache, uri, fakeDataSet.getData(uri).getData());
}
}
/** Asserts that the cache contains the given data for {@code uriString}. */
- public static void assertDataCached(Cache cache, String uriString, byte[] expected)
- throws IOException {
+ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException {
CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
DataSourceInputStream inputStream = new DataSourceInputStream(dataSource,
- new DataSpec(Uri.parse(uriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH));
+ new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH));
try {
inputStream.open();
byte[] buffer = new byte[1024];
@@ -87,7 +98,7 @@ public final class CacheAsserts {
} finally {
inputStream.close();
}
- MoreAsserts.assertEquals("Cached data doesn't match expected for '" + uriString + "',",
+ MoreAsserts.assertEquals("Cached data doesn't match expected for '" + uri + "',",
expected, outputStream.toByteArray());
}
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java
index b61b484e32..c039dd3283 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.testutil;
+import android.os.ConditionVariable;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
@@ -72,6 +73,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
private final long expectedPlayingTimeMs;
private final DecoderCounters videoDecoderCounters;
private final DecoderCounters audioDecoderCounters;
+ private final ConditionVariable testFinished;
private ActionSchedule pendingSchedule;
private Handler actionHandler;
@@ -81,7 +83,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
private ExoPlaybackException playerError;
private Player.EventListener playerEventListener;
private boolean playerWasPrepared;
- private boolean playerFinished;
+
private boolean playing;
private long totalPlayingTimeMs;
private long lastPlayingStartTimeMs;
@@ -114,8 +116,9 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
this.tag = tag;
this.expectedPlayingTimeMs = expectedPlayingTimeMs;
this.failOnPlayerError = failOnPlayerError;
- videoDecoderCounters = new DecoderCounters();
- audioDecoderCounters = new DecoderCounters();
+ this.testFinished = new ConditionVariable();
+ this.videoDecoderCounters = new DecoderCounters();
+ this.audioDecoderCounters = new DecoderCounters();
}
/**
@@ -169,16 +172,13 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
}
@Override
- public final boolean canStop() {
- return playerFinished;
+ public final boolean blockUntilStopped(long timeoutMs) {
+ return testFinished.block(timeoutMs);
}
@Override
- public final void onStop() {
- actionHandler.removeCallbacksAndMessages(null);
- sourceDurationMs = player.getDuration();
- player.release();
- player = null;
+ public final boolean forceStop() {
+ return stopTest();
}
@Override
@@ -219,7 +219,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
playerWasPrepared |= playbackState != Player.STATE_IDLE;
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playerWasPrepared)) {
- playerFinished = true;
+ stopTest();
}
boolean playing = playWhenReady && playbackState == Player.STATE_READY;
if (!this.playing && playing) {
@@ -235,6 +235,11 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
// Do nothing.
}
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ // Do nothing.
+ }
+
@Override
public final void onPlayerError(ExoPlaybackException error) {
playerWasPrepared = true;
@@ -334,6 +339,25 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener,
// Internal logic
+ private boolean stopTest() {
+ if (player == null) {
+ return false;
+ }
+ actionHandler.removeCallbacksAndMessages(null);
+ sourceDurationMs = player.getDuration();
+ player.release();
+ player = null;
+ // We post opening of the finished condition so that any events posted to the main thread as a
+ // result of player.release() are guaranteed to be handled before the test returns.
+ actionHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ testFinished.open();
+ }
+ });
+ return true;
+ }
+
protected DrmSessionManager buildDrmSessionManager(String userAgent) {
// Do nothing. Interested subclasses may override.
return null;
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java
new file mode 100644
index 0000000000..2b5ea11d94
--- /dev/null
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java
@@ -0,0 +1,384 @@
+/*
+ * 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.testutil;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import com.google.android.exoplayer2.DefaultLoadControl;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.text.TextOutput;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.util.LinkedList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import junit.framework.Assert;
+
+/**
+ * Helper class to run an ExoPlayer test.
+ */
+public final class ExoPlayerTestRunner implements Player.EventListener {
+
+ /**
+ * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for
+ * unset test properties.
+ */
+ public static final class Builder {
+
+ /**
+ * Factory to create an {@link SimpleExoPlayer} instance. The player will be created on its own
+ * {@link HandlerThread}.
+ */
+ public interface PlayerFactory {
+
+ SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory,
+ MappingTrackSelector trackSelector, LoadControl loadControl);
+
+ }
+
+ public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null,
+ MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
+ null, null);
+ public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null,
+ MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
+
+ private PlayerFactory playerFactory;
+ private Timeline timeline;
+ private Object manifest;
+ private MediaSource mediaSource;
+ private MappingTrackSelector trackSelector;
+ private LoadControl loadControl;
+ private Format[] supportedFormats;
+ private Renderer[] renderers;
+ private RenderersFactory renderersFactory;
+ private ActionSchedule actionSchedule;
+ private Player.EventListener eventListener;
+
+ public Builder setTimeline(Timeline timeline) {
+ Assert.assertNull(mediaSource);
+ this.timeline = timeline;
+ return this;
+ }
+
+ public Builder setManifest(Object manifest) {
+ Assert.assertNull(mediaSource);
+ this.manifest = manifest;
+ return this;
+ }
+
+ /** Replaces {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. */
+ public Builder setMediaSource(MediaSource mediaSource) {
+ Assert.assertNull(timeline);
+ Assert.assertNull(manifest);
+ this.mediaSource = mediaSource;
+ return this;
+ }
+
+ public Builder setTrackSelector(MappingTrackSelector trackSelector) {
+ this.trackSelector = trackSelector;
+ return this;
+ }
+
+ public Builder setLoadControl(LoadControl loadControl) {
+ this.loadControl = loadControl;
+ return this;
+ }
+
+ public Builder setSupportedFormats(Format... supportedFormats) {
+ this.supportedFormats = supportedFormats;
+ return this;
+ }
+
+ public Builder setRenderers(Renderer... renderers) {
+ Assert.assertNull(renderersFactory);
+ this.renderers = renderers;
+ return this;
+ }
+
+ /** Replaces {@link #setRenderers(Renderer...)}. */
+ public Builder setRenderersFactory(RenderersFactory renderersFactory) {
+ Assert.assertNull(renderers);
+ this.renderersFactory = renderersFactory;
+ return this;
+ }
+
+ public Builder setExoPlayer(PlayerFactory playerFactory) {
+ this.playerFactory = playerFactory;
+ return this;
+ }
+
+ public Builder setActionSchedule(ActionSchedule actionSchedule) {
+ this.actionSchedule = actionSchedule;
+ return this;
+ }
+
+ public Builder setEventListener(Player.EventListener eventListener) {
+ this.eventListener = eventListener;
+ return this;
+ }
+
+ public ExoPlayerTestRunner build() {
+ if (supportedFormats == null) {
+ supportedFormats = new Format[] { VIDEO_FORMAT };
+ }
+ if (trackSelector == null) {
+ trackSelector = new DefaultTrackSelector();
+ }
+ if (renderersFactory == null) {
+ if (renderers == null) {
+ renderers = new Renderer[] { new FakeRenderer(supportedFormats) };
+ }
+ renderersFactory = new RenderersFactory() {
+ @Override
+ public Renderer[] createRenderers(Handler eventHandler,
+ VideoRendererEventListener videoRendererEventListener,
+ AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput) {
+ return renderers;
+ }
+ };
+ }
+ if (loadControl == null) {
+ loadControl = new DefaultLoadControl();
+ }
+ if (playerFactory == null) {
+ playerFactory = new PlayerFactory() {
+ @Override
+ public SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory,
+ MappingTrackSelector trackSelector, LoadControl loadControl) {
+ return ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ }
+ };
+ }
+ if (mediaSource == null) {
+ if (timeline == null) {
+ timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
+ }
+ mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats);
+ }
+ return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector,
+ loadControl, actionSchedule, eventListener);
+ }
+ }
+
+ private final PlayerFactory playerFactory;
+ private final MediaSource mediaSource;
+ private final RenderersFactory renderersFactory;
+ private final MappingTrackSelector trackSelector;
+ private final LoadControl loadControl;
+ private final ActionSchedule actionSchedule;
+ private final Player.EventListener eventListener;
+
+ private final HandlerThread playerThread;
+ private final Handler handler;
+ private final CountDownLatch endedCountDownLatch;
+ private final LinkedList timelines;
+ private final LinkedList