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 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 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 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 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}.
    • + * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@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 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 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 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 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 none Repeat one Repeat 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 manifests; + private final LinkedList periodIndices; + + private SimpleExoPlayer player; + private Exception exception; + private TrackGroupArray trackGroups; + private int positionDiscontinuityCount; + + private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, + RenderersFactory renderersFactory, MappingTrackSelector trackSelector, + LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener) { + this.playerFactory = playerFactory; + this.mediaSource = mediaSource; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.actionSchedule = actionSchedule; + this.eventListener = eventListener; + this.timelines = new LinkedList<>(); + this.manifests = new LinkedList<>(); + this.periodIndices = new LinkedList<>(); + this.endedCountDownLatch = new CountDownLatch(1); + this.playerThread = new HandlerThread("ExoPlayerTest thread"); + playerThread.start(); + this.handler = new Handler(playerThread.getLooper()); + } + + // Called on the test thread to run the test. + + public ExoPlayerTestRunner start() { + handler.post(new Runnable() { + @Override + public void run() { + try { + player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player.addListener(ExoPlayerTestRunner.this); + if (eventListener != null) { + player.addListener(eventListener); + } + player.setPlayWhenReady(true); + if (actionSchedule != null) { + actionSchedule.start(player, trackSelector, null, handler); + } + player.prepare(mediaSource); + } catch (Exception e) { + handleException(e); + } + } + }); + return this; + } + + public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + exception = new TimeoutException("Test playback timed out waiting for playback to end."); + } + release(); + // Throw any pending exception (from playback, timing out or releasing). + if (exception != null) { + throw exception; + } + return this; + } + + // Assertions called on the test thread after test finished. + + public void assertTimelinesEqual(Timeline... timelines) { + Assert.assertEquals(timelines.length, this.timelines.size()); + for (Timeline timeline : timelines) { + Assert.assertEquals(timeline, this.timelines.remove()); + } + } + + public void assertManifestsEqual(Object... manifests) { + Assert.assertEquals(manifests.length, this.manifests.size()); + for (Object manifest : manifests) { + Assert.assertEquals(manifest, this.manifests.remove()); + } + } + + public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { + Assert.assertEquals(trackGroupArray, this.trackGroups); + } + + public void assertPositionDiscontinuityCount(int expectedCount) { + Assert.assertEquals(expectedCount, positionDiscontinuityCount); + } + + public void assertPlayedPeriodIndices(int... periodIndices) { + Assert.assertEquals(periodIndices.length, this.periodIndices.size()); + for (int periodIndex : periodIndices) { + Assert.assertEquals(periodIndex, (int) this.periodIndices.remove()); + } + } + + // Private implementation details. + + private void release() throws InterruptedException { + handler.post(new Runnable() { + @Override + public void run() { + try { + if (player != null) { + player.release(); + } + } catch (Exception e) { + handleException(e); + } finally { + playerThread.quit(); + } + } + }); + playerThread.join(); + } + + private void handleException(Exception exception) { + if (this.exception == null) { + this.exception = exception; + } + endedCountDownLatch.countDown(); + } + + // Player.EventListener + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + timelines.add(timeline); + manifests.add(manifest); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + this.trackGroups = trackGroups; + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { + periodIndices.add(player.getCurrentPeriodIndex()); + } + if (playbackState == Player.STATE_ENDED) { + endedCountDownLatch.countDown(); + } + } + + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + // Do nothing. + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + handleException(exception); + } + + @Override + public void onPositionDiscontinuity() { + positionDiscontinuityCount++; + periodIndices.add(player.getCurrentPeriodIndex()); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java deleted file mode 100644 index ab247283e6..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.testutil; - -import android.os.Handler; -import android.os.HandlerThread; -import android.util.Pair; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.util.LinkedList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import junit.framework.Assert; - -/** - * Wraps a player with its own handler thread. - */ -public class ExoPlayerWrapper implements Player.EventListener { - - private final CountDownLatch sourceInfoCountDownLatch; - private final CountDownLatch endedCountDownLatch; - private final HandlerThread playerThread; - private final Handler handler; - private final LinkedList> sourceInfos; - - public ExoPlayer player; - public TrackGroupArray trackGroups; - public Exception exception; - - // Written only on the main thread. - public volatile int positionDiscontinuityCount; - - public ExoPlayerWrapper() { - sourceInfoCountDownLatch = new CountDownLatch(1); - endedCountDownLatch = new CountDownLatch(1); - playerThread = new HandlerThread("ExoPlayerTest thread"); - playerThread.start(); - handler = new Handler(playerThread.getLooper()); - sourceInfos = new LinkedList<>(); - } - - // Called on the test thread. - - public void blockUntilEnded(long timeoutMs) throws Exception { - if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - exception = new TimeoutException("Test playback timed out waiting for playback to end."); - } - release(); - // Throw any pending exception (from playback, timing out or releasing). - if (exception != null) { - throw exception; - } - } - - public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { - if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("Test playback timed out waiting for source info."); - } - } - - public void setup(final MediaSource mediaSource, final Renderer... renderers) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); - player.addListener(ExoPlayerWrapper.this); - player.setPlayWhenReady(true); - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void prepare(final MediaSource mediaSource) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void release() throws InterruptedException { - handler.post(new Runnable() { - @Override - public void run() { - try { - if (player != null) { - player.release(); - } - } catch (Exception e) { - handleError(e); - } finally { - playerThread.quit(); - } - } - }); - playerThread.join(); - } - - private void handleError(Exception exception) { - if (this.exception == null) { - this.exception = exception; - } - endedCountDownLatch.countDown(); - } - - @SafeVarargs - public final void assertSourceInfosEquals(Pair... sourceInfos) { - Assert.assertEquals(sourceInfos.length, this.sourceInfos.size()); - for (Pair sourceInfo : sourceInfos) { - Assert.assertEquals(sourceInfo, this.sourceInfos.remove()); - } - } - - // Player.EventListener implementation. - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - endedCountDownLatch.countDown(); - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - sourceInfos.add(Pair.create(timeline, manifest)); - sourceInfoCountDownLatch.countDown(); - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - this.trackGroups = trackGroups; - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - handleError(exception); - } - - @SuppressWarnings("NonAtomicVolatileUpdate") - @Override - public void onPositionDiscontinuity() { - positionDiscontinuityCount++; - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - -} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java index f4476ddf93..add0c5d22f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.TrackGroup; /** * Fake data set emulating the data of an adaptive media source. - * It provides chunk data for all {@link Format}s in the given {@link TrackSelection}. + * It provides chunk data for all {@link Format}s in the given {@link TrackGroup}. */ public final class FakeAdaptiveDataSet extends FakeDataSet { @@ -36,8 +36,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { this.chunkDurationUs = chunkDurationUs; } - public FakeAdaptiveDataSet createDataSet(TrackSelection trackSelection, long mediaDurationUs) { - return new FakeAdaptiveDataSet(trackSelection, mediaDurationUs, chunkDurationUs); + public FakeAdaptiveDataSet createDataSet(TrackGroup trackGroup, long mediaDurationUs) { + return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs); } } @@ -46,15 +46,14 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { private final long chunkDurationUs; private final long lastChunkDurationUs; - public FakeAdaptiveDataSet(TrackSelection trackSelection, long mediaDurationUs, - long chunkDurationUs) { + public FakeAdaptiveDataSet(TrackGroup trackGroup, long mediaDurationUs, long chunkDurationUs) { this.chunkDurationUs = chunkDurationUs; - int selectionCount = trackSelection.length(); + int trackCount = trackGroup.length; long lastChunkDurationUs = mediaDurationUs % chunkDurationUs; int fullChunks = (int) (mediaDurationUs / chunkDurationUs); - for (int i = 0; i < selectionCount; i++) { + for (int i = 0; i < trackCount; i++) { String uri = getUri(i); - Format format = trackSelection.getFormat(i); + Format format = trackGroup.getFormat(i); int chunkLength = (int) (format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND)); FakeData newData = this.newData(uri); for (int j = 0; j < fullChunks; j++) { @@ -74,8 +73,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { return chunkCount; } - public String getUri(int trackSelectionIndex) { - return "fake://adaptive.media/" + Integer.toString(trackSelectionIndex); + public String getUri(int trackIndex) { + return "fake://adaptive.media/" + trackIndex; } public long getChunkDuration(int chunkIndex) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java new file mode 100644 index 0000000000..c8757e69cd --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -0,0 +1,118 @@ +/* + * 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 com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SequenceableLoader; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.List; + +/** + * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting a + * track will give the player a {@link ChunkSampleStream}. + */ +public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod + implements SequenceableLoader.Callback> { + + private final EventDispatcher eventDispatcher; + private final Allocator allocator; + private final FakeChunkSource.Factory chunkSourceFactory; + private final long durationUs; + + private Callback callback; + private ChunkSampleStream[] sampleStreams; + private SequenceableLoader sequenceableLoader; + + public FakeAdaptiveMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, + Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, long durationUs) { + super(trackGroupArray); + this.eventDispatcher = eventDispatcher; + this.allocator = allocator; + this.chunkSourceFactory = chunkSourceFactory; + this.durationUs = durationUs; + } + + @Override + public void prepare(Callback callback, long positionUs) { + super.prepare(callback, positionUs); + this.callback = callback; + } + + @Override + @SuppressWarnings("unchecked") + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams, + streamResetFlags, positionUs); + List> validStreams = new ArrayList<>(); + for (SampleStream stream : streams) { + if (stream != null) { + validStreams.add((ChunkSampleStream) stream); + } + } + this.sampleStreams = validStreams.toArray(new ChunkSampleStream[validStreams.size()]); + this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + return returnPositionUs; + } + + @Override + public long getBufferedPositionUs() { + super.getBufferedPositionUs(); + return sequenceableLoader.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.seekToUs(positionUs); + } + return super.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + super.getNextLoadPositionUs(); + return sequenceableLoader.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + super.continueLoading(positionUs); + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + protected SampleStream createSampleStream(TrackSelection trackSelection) { + FakeChunkSource chunkSource = chunkSourceFactory.createChunkSource(trackSelection, durationUs); + return new ChunkSampleStream<>( + MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), null, + chunkSource, this, allocator, 0, 3, eventDispatcher); + } + + @Override + public void onContinueLoadingRequested(ChunkSampleStream source) { + callback.onContinueLoadingRequested(this); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java new file mode 100644 index 0000000000..59bcaf3e7c --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -0,0 +1,52 @@ +/* + * 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 com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.Allocator; + +/** + * Fake {@link MediaSource} that provides a given timeline. Creating the period returns a + * {@link FakeAdaptiveMediaPeriod} from the given {@link TrackGroupArray}. + */ +public class FakeAdaptiveMediaSource extends FakeMediaSource { + + private final EventDispatcher eventDispatcher; + private final FakeChunkSource.Factory chunkSourceFactory; + + public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, + TrackGroupArray trackGroupArray, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + super(timeline, manifest, trackGroupArray); + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.chunkSourceFactory = chunkSourceFactory; + } + + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, + Allocator allocator) { + Period period = timeline.getPeriod(id.periodIndex, new Period()); + return new FakeAdaptiveMediaPeriod(trackGroupArray, eventDispatcher, allocator, + chunkSourceFactory, period.durationUs); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 0c970caa15..b8f25bfbce 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -50,7 +50,8 @@ public final class FakeChunkSource implements ChunkSource { } public FakeChunkSource createChunkSource(TrackSelection trackSelection, long durationUs) { - FakeAdaptiveDataSet dataSet = dataSetFactory.createDataSet(trackSelection, durationUs); + FakeAdaptiveDataSet dataSet = + dataSetFactory.createDataSet(trackSelection.getTrackGroup(), durationUs); dataSourceFactory.setFakeDataSet(dataSet); DataSource dataSource = dataSourceFactory.createDataSource(); return new FakeChunkSource(trackSelection, dataSource, dataSet); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index 2580205361..e77e0714e7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSpec; @@ -28,11 +29,11 @@ import java.util.List; /** * Collection of {@link FakeData} to be served by a {@link FakeDataSource}. * - *

    Multiple fake data can be defined by {@link FakeDataSet#setData(String, byte[])} and {@link - * FakeDataSet#newData(String)} methods. It's also possible to define a default data by {@link + *

    Multiple fake data can be defined by {@link FakeDataSet#setData(Uri, byte[])} and {@link + * FakeDataSet#newData(Uri)} methods. It's also possible to define a default data by {@link * FakeDataSet#newDefaultData()}. * - *

    {@link FakeDataSet#newData(String)} and {@link FakeDataSet#newDefaultData()} return a {@link + *

    {@link FakeDataSet#newData(Uri)} and {@link FakeDataSet#newDefaultData()} return a {@link * FakeData} instance which can be used to define specific results during * {@link FakeDataSource#read(byte[], int, int)} calls. * @@ -104,8 +105,8 @@ public class FakeDataSet { this(null, 0, null, action, previousSegment); } - private Segment(byte[] data, int length, IOException exception, Runnable action, - Segment previousSegment) { + private Segment(@Nullable byte[] data, int length, @Nullable IOException exception, + @Nullable Runnable action, Segment previousSegment) { this.exception = exception; this.action = action; this.data = data; @@ -125,12 +126,12 @@ public class FakeDataSet { } /** Uri of the data or null if this is the default FakeData. */ - public final String uri; + public final Uri uri; private final ArrayList segments; private final FakeDataSet dataSet; private boolean simulateUnknownLength; - private FakeData(FakeDataSet dataSet, String uri) { + private FakeData(FakeDataSet dataSet, Uri uri) { this.uri = uri; this.segments = new ArrayList<>(); this.dataSet = dataSet; @@ -162,8 +163,8 @@ public class FakeDataSet { } /** - * Appends data of the specified length. No actual data is available and this data should not - * be read. + * Appends a data segment of the specified length. No actual data is available and the + * {@link FakeDataSource} will perform no copy operations when this data is read. */ public FakeData appendReadData(int length) { Assertions.checkState(length > 0); @@ -219,7 +220,7 @@ public class FakeDataSet { } - private final HashMap dataMap; + private final HashMap dataMap; private FakeData defaultData; public FakeDataSet() { @@ -234,16 +235,31 @@ public class FakeDataSet { /** Sets random data with the given {@code length} for the given {@code uri}. */ public FakeDataSet setRandomData(String uri, int length) { + return setRandomData(Uri.parse(uri), length); + } + + /** Sets random data with the given {@code length} for the given {@code uri}. */ + public FakeDataSet setRandomData(Uri uri, int length) { return setData(uri, TestUtil.buildTestData(length)); } /** Sets the given {@code data} for the given {@code uri}. */ public FakeDataSet setData(String uri, byte[] data) { + return setData(Uri.parse(uri), data); + } + + /** Sets the given {@code data} for the given {@code uri}. */ + public FakeDataSet setData(Uri uri, byte[] data) { return newData(uri).appendReadData(data).endData(); } /** Returns a new {@link FakeData} with the given {@code uri}. */ public FakeData newData(String uri) { + return newData(Uri.parse(uri)); + } + + /** Returns a new {@link FakeData} with the given {@code uri}. */ + public FakeData newData(Uri uri) { FakeData data = new FakeData(this, uri); dataMap.put(uri, data); return data; @@ -251,6 +267,11 @@ public class FakeDataSet { /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */ public FakeData getData(String uri) { + return getData(Uri.parse(uri)); + } + + /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */ + public FakeData getData(Uri uri) { FakeData data = dataMap.get(uri); return data != null ? data : defaultData; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 6180a8aa77..aacd265e45 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -166,6 +166,7 @@ public class FakeDataSource implements DataSource { // Do not allow crossing of the segment boundary. readLength = Math.min(readLength, current.length - current.bytesRead); // Perform the read and return. + Assertions.checkArgument(buffer.length - offset >= readLength); if (current.data != null) { System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d8e501a298..3863cf7987 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; @@ -26,10 +25,10 @@ import java.io.IOException; import junit.framework.Assert; /** - * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that - * track will give the player a {@link FakeSampleStream}. + * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting + * tracks will give the player {@link FakeSampleStream}s. */ -public final class FakeMediaPeriod implements MediaPeriod { +public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; @@ -46,7 +45,6 @@ public final class FakeMediaPeriod implements MediaPeriod { @Override public void prepare(Callback callback, long positionUs) { Assert.assertFalse(preparedPeriod); - Assert.assertEquals(0, positionUs); preparedPeriod = true; callback.onPrepared(this); } @@ -71,8 +69,6 @@ public final class FakeMediaPeriod implements MediaPeriod { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { streams[i] = null; } - } - for (int i = 0; i < rendererCount; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; Assert.assertTrue(1 <= selection.length()); @@ -81,7 +77,7 @@ public final class FakeMediaPeriod implements MediaPeriod { int indexInTrackGroup = selection.getIndexInTrackGroup(selection.getSelectedIndex()); Assert.assertTrue(0 <= indexInTrackGroup); Assert.assertTrue(indexInTrackGroup < trackGroup.length); - streams[i] = new FakeSampleStream(selection.getSelectedFormat()); + streams[i] = createSampleStream(selection); streamResetFlags[i] = true; } } @@ -123,4 +119,8 @@ public final class FakeMediaPeriod implements MediaPeriod { return false; } + protected SampleStream createSampleStream(TrackSelection selection) { + return new FakeSampleStream(selection.getSelectedFormat()); + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index a2c1e9879e..9e7b498269 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -34,7 +34,7 @@ import junit.framework.Assert; */ public class FakeMediaSource implements MediaSource { - private final Timeline timeline; + protected final Timeline timeline; private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; @@ -82,7 +82,7 @@ public class FakeMediaSource implements MediaSource { Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); Assert.assertTrue(preparedSource); Assert.assertFalse(releasedSource); - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); return mediaPeriod; } @@ -104,6 +104,11 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, + Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray); + } + private static TrackGroupArray buildTrackGroupArray(Format... formats) { TrackGroup[] trackGroups = new TrackGroup[formats.length]; for (int i = 0; i < formats.length; i++) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 4e1e32980f..699b850f73 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -60,8 +60,8 @@ public final class FakeSampleStream implements SampleStream { } @Override - public void skipData(long positionUs) { - // Do nothing. + public int skipData(long positionUs) { + return 0; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java new file mode 100644 index 0000000000..7edaa6b13e --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -0,0 +1,531 @@ +/* + * 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.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +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.RendererCapabilities; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Fake {@link SimpleExoPlayer} which runs a simplified copy of the playback loop as fast as + * possible without waiting. It does only support single period timelines and does not support + * updates during playback (like seek, timeline changes, repeat mode changes). + */ +public class FakeSimpleExoPlayer extends SimpleExoPlayer { + + private FakeExoPlayer player; + + public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, + LoadControl loadControl, FakeClock clock) { + super (renderersFactory, trackSelector, loadControl); + player.setFakeClock(clock); + } + + @Override + protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); + return player; + } + + private class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, MediaPeriod.Callback, + Runnable { + + private final Renderer[] renderers; + private final TrackSelector trackSelector; + private final LoadControl loadControl; + private final CopyOnWriteArraySet eventListeners; + private final HandlerThread playbackThread; + private final Handler playbackHandler; + private final Handler eventListenerHandler; + + private FakeClock clock; + private MediaSource mediaSource; + private Timeline timeline; + private Object manifest; + private MediaPeriod mediaPeriod; + private TrackSelectorResult selectorResult; + + private boolean isStartingUp; + private boolean isLoading; + private int playbackState; + private long rendererPositionUs; + private long durationUs; + private volatile long currentPositionMs; + private volatile long bufferedPositionMs; + + public FakeExoPlayer(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + this.renderers = renderers; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.eventListeners = new CopyOnWriteArraySet<>(); + Looper eventListenerLooper = Looper.myLooper(); + this.eventListenerHandler = new Handler(eventListenerLooper != null ? eventListenerLooper + : Looper.getMainLooper()); + this.playbackThread = new HandlerThread("FakeExoPlayer Thread"); + playbackThread.start(); + this.playbackHandler = new Handler(playbackThread.getLooper()); + this.isStartingUp = true; + this.isLoading = false; + this.playbackState = Player.STATE_IDLE; + this.durationUs = C.TIME_UNSET; + } + + public void setFakeClock(FakeClock clock) { + this.clock = clock; + } + + @Override + public void addListener(Player.EventListener listener) { + eventListeners.add(listener); + } + + @Override + public void removeListener(Player.EventListener listener) { + eventListeners.remove(listener); + } + + @Override + public int getPlaybackState() { + return playbackState; + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + if (playWhenReady != true) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getPlayWhenReady() { + return true; + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + return Player.REPEAT_MODE_OFF; + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getShuffleModeEnabled() { + return false; + } + + @Override + public boolean isLoading() { + return isLoading; + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + playbackThread.quit(); + } + + @Override + public int getRendererCount() { + return renderers.length; + } + + @Override + public int getRendererType(int index) { + return renderers[index].getTrackType(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return selectorResult != null ? selectorResult.groups : null; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return selectorResult != null ? selectorResult.selections : null; + } + + @Nullable + @Override + public Object getCurrentManifest() { + return manifest; + } + + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public long getDuration() { + return C.usToMs(durationUs); + } + + @Override + public long getCurrentPosition() { + return currentPositionMs; + } + + @Override + public long getBufferedPosition() { + return bufferedPositionMs == C.TIME_END_OF_SOURCE ? getDuration() : bufferedPositionMs; + } + + @Override + public int getBufferedPercentage() { + long duration = getDuration(); + return duration == C.TIME_UNSET ? 0 : (int) (getBufferedPosition() * 100 / duration); + } + + @Override + public boolean isCurrentWindowDynamic() { + return false; + } + + @Override + public boolean isCurrentWindowSeekable() { + return false; + } + + @Override + public boolean isPlayingAd() { + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return 0; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return 0; + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + @Override + public Looper getPlaybackLooper() { + return playbackThread.getLooper(); + } + + @Override + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, true, true); + } + + @Override + public void prepare(final MediaSource mediaSource, boolean resetPosition, boolean resetState) { + if (resetPosition != true || resetState != true) { + throw new UnsupportedOperationException(); + } + this.mediaSource = mediaSource; + playbackHandler.post(new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this); + } + }); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + // MediaSource.Listener + + @Override + public void onSourceInfoRefreshed(final Timeline timeline, final @Nullable Object manifest) { + if (this.timeline != null) { + throw new UnsupportedOperationException(); + } + Assertions.checkArgument(timeline.getPeriodCount() == 1); + Assertions.checkArgument(timeline.getWindowCount() == 1); + final ConditionVariable waitForNotification = new ConditionVariable(); + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; + FakeExoPlayer.this.timeline = timeline; + FakeExoPlayer.this.manifest = manifest; + eventListener.onTimelineChanged(timeline, manifest); + waitForNotification.open(); + } + } + }); + waitForNotification.block(); + this.mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), loadControl.getAllocator()); + mediaPeriod.prepare(this, 0); + } + + // MediaPeriod.Callback + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + maybeContinueLoading(); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + try { + initializePlaybackLoop(); + } catch (ExoPlaybackException e) { + handlePlayerError(e); + } + } + + // Runnable (Playback loop). + + @Override + public void run() { + try { + maybeContinueLoading(); + boolean allRenderersEnded = true; + boolean allRenderersReadyOrEnded = true; + if (playbackState == Player.STATE_READY) { + for (Renderer renderer : renderers) { + renderer.render(rendererPositionUs, C.msToUs(clock.elapsedRealtime())); + if (!renderer.isEnded()) { + allRenderersEnded = false; + } + if (!(renderer.isReady() || renderer.isEnded())) { + allRenderersReadyOrEnded = false; + } + } + } + if (rendererPositionUs >= durationUs && allRenderersEnded) { + changePlaybackState(Player.STATE_ENDED); + return; + } + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + if (playbackState == Player.STATE_BUFFERING && allRenderersReadyOrEnded + && haveSufficientBuffer(!isStartingUp, rendererPositionUs, bufferedPositionUs)) { + changePlaybackState(Player.STATE_READY); + isStartingUp = false; + } else if (playbackState == Player.STATE_READY && !allRenderersReadyOrEnded) { + changePlaybackState(Player.STATE_BUFFERING); + } + // Advance simulated time by 10ms. + clock.advanceTime(10); + if (playbackState == Player.STATE_READY) { + rendererPositionUs += 10000; + } + this.currentPositionMs = C.usToMs(rendererPositionUs); + this.bufferedPositionMs = C.usToMs(bufferedPositionUs); + playbackHandler.post(this); + } catch (ExoPlaybackException e) { + handlePlayerError(e); + } + } + + // Internal logic + + private void initializePlaybackLoop() throws ExoPlaybackException { + Assertions.checkNotNull(clock); + trackSelector.init(new InvalidationListener() { + @Override + public void onTrackSelectionsInvalidated() { + throw new IllegalStateException(); + } + }); + RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + rendererCapabilities[i] = renderers[i].getCapabilities(); + } + selectorResult = trackSelector.selectTracks(rendererCapabilities, + mediaPeriod.getTrackGroups()); + SampleStream[] sampleStreams = new SampleStream[renderers.length]; + boolean[] mayRetainStreamFlags = new boolean[renderers.length]; + Arrays.fill(mayRetainStreamFlags, true); + mediaPeriod.selectTracks(selectorResult.selections.getAll(), mayRetainStreamFlags, + sampleStreams, new boolean[renderers.length], 0); + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + eventListener.onTracksChanged(selectorResult.groups, selectorResult.selections); + } + } + }); + + loadControl.onPrepared(); + loadControl.onTracksSelected(renderers, selectorResult.groups, selectorResult.selections); + + for (int i = 0; i < renderers.length; i++) { + TrackSelection selection = selectorResult.selections.get(i); + Format[] formats = new Format[selection.length()]; + for (int j = 0; j < formats.length; j++) { + formats[j] = selection.getFormat(j); + } + renderers[i].enable(selectorResult.rendererConfigurations[i], formats, sampleStreams[i], 0, + false, 0); + renderers[i].setCurrentStreamFinal(); + } + + rendererPositionUs = 0; + changePlaybackState(Player.STATE_BUFFERING); + playbackHandler.post(this); + } + + private void maybeContinueLoading() { + boolean newIsLoading = false; + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { + long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; + if (loadControl.shouldContinueLoading(bufferedDurationUs)) { + newIsLoading = true; + mediaPeriod.continueLoading(rendererPositionUs); + } + } + if (newIsLoading != isLoading) { + isLoading = newIsLoading; + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + eventListener.onLoadingChanged(isLoading); + } + } + }); + } + } + + private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, + long bufferedPositionUs) { + if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { + return true; + } + return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, rebuffering); + } + + private void handlePlayerError(final ExoPlaybackException e) { + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener listener : eventListeners) { + listener.onPlayerError(e); + } + } + }); + changePlaybackState(Player.STATE_ENDED); + } + + private void changePlaybackState(final int playbackState) { + this.playbackState = playbackState; + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener listener : eventListeners) { + listener.onPlayerStateChanged(true, playbackState); + } + } + }); + } + + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 831344aa8b..66b992e652 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -17,14 +17,12 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.fail; -import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.Bundle; import android.os.ConditionVariable; -import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; @@ -33,7 +31,6 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; /** * A host activity for performing playback tests. @@ -57,19 +54,20 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba void onStart(HostActivity host, Surface surface); /** - * Called on the main thread to check whether the test is ready to be stopped. + * Called on the main thread to block until the test has stopped or {@link #forceStop()} is + * called. * - * @return Whether the test is ready to be stopped. + * @param timeoutMs The maximum time to block in milliseconds. + * @return Whether the test has stopped successful. */ - boolean canStop(); + boolean blockUntilStopped(long timeoutMs); /** - * Called on the main thread when the test is stopped. - *

    - * The test will be stopped if {@link #canStop()} returns true, if the {@link HostActivity} has - * been paused, or if the {@link HostActivity}'s {@link Surface} has been destroyed. + * Called on the main thread to force stop the test (if it is not stopped already). + * + * @return Whether the test was forced stopped. */ - void onStop(); + boolean forceStop(); /** * Called on the test thread after the test has finished and been stopped. @@ -85,13 +83,11 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba private WakeLock wakeLock; private WifiLock wifiLock; private SurfaceView surfaceView; - private Handler mainHandler; - private CheckCanStopRunnable checkCanStopRunnable; private HostedTest hostedTest; - private ConditionVariable hostedTestStoppedCondition; private boolean hostedTestStarted; - private boolean hostedTestFinished; + private ConditionVariable hostedTestStartedCondition; + private boolean forcedStopped; /** * Executes a {@link HostedTest} inside the host. @@ -100,7 +96,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * @param timeoutMs The number of milliseconds to wait for the test to finish. If the timeout * is exceeded then the test will fail. */ - public void runTest(final HostedTest hostedTest, long timeoutMs) { + public void runTest(HostedTest hostedTest, long timeoutMs) { runTest(hostedTest, timeoutMs, true); } @@ -114,27 +110,28 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba public void runTest(final HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { Assertions.checkArgument(timeoutMs > 0); Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); - Assertions.checkState(this.hostedTest == null); - this.hostedTest = Assertions.checkNotNull(hostedTest); - hostedTestStoppedCondition = new ConditionVariable(); + Assertions.checkNotNull(hostedTest); + hostedTestStartedCondition = new ConditionVariable(); + forcedStopped = false; hostedTestStarted = false; - hostedTestFinished = false; runOnUiThread(new Runnable() { @Override public void run() { + HostActivity.this.hostedTest = hostedTest; maybeStartHostedTest(); } }); + hostedTestStartedCondition.block(); - if (hostedTestStoppedCondition.block(timeoutMs)) { - if (hostedTestFinished) { - Log.d(TAG, "Test finished. Checking pass conditions."); + if (hostedTest.blockUntilStopped(timeoutMs)) { + if (!forcedStopped) { + Log.d(TAG, "Checking test pass conditions."); hostedTest.onFinished(); Log.d(TAG, "Pass conditions checked."); } else { - String message = "Test released before it finished. Activity may have been paused whilst " + String message = "Test force stopped. Activity may have been paused whilst " + "test was in progress."; Log.e(TAG, message); fail(message); @@ -145,9 +142,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba if (failOnTimeout) { fail(message); } - maybeStopHostedTest(); - hostedTestStoppedCondition.block(); } + this.hostedTest = null; } // Activity lifecycle @@ -160,15 +156,13 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba surfaceView = (SurfaceView) findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); - mainHandler = new Handler(); - checkCanStopRunnable = new CheckCanStopRunnable(); } @Override public void onStart() { Context appContext = getApplicationContext(); WifiManager wifiManager = (WifiManager) appContext.getSystemService(Context.WIFI_SERVICE); - wifiLock = wifiManager.createWifiLock(getWifiLockMode(), TAG); + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TAG); wifiLock.acquire(); PowerManager powerManager = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); @@ -176,12 +170,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba super.onStart(); } - @Override - public void onResume() { - super.onResume(); - maybeStartHostedTest(); - } - @Override public void onPause() { super.onPause(); @@ -225,50 +213,14 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba hostedTestStarted = true; Log.d(TAG, "Starting test."); hostedTest.onStart(this, surface); - checkCanStopRunnable.startChecking(); + hostedTestStartedCondition.open(); } } private void maybeStopHostedTest() { - if (hostedTest != null && hostedTestStarted) { - hostedTest.onStop(); - hostedTest = null; - mainHandler.removeCallbacks(checkCanStopRunnable); - // We post opening of the stopped condition so that any events posted to the main thread as a - // result of hostedTest.onStop() are guaranteed to be handled before hostedTest.onFinished() - // is called from runTest. - mainHandler.post(new Runnable() { - @Override - public void run() { - hostedTestStoppedCondition.open(); - } - }); + if (hostedTest != null && hostedTestStarted && !forcedStopped) { + forcedStopped = hostedTest.forceStop(); } } - @SuppressLint("InlinedApi") - private static int getWifiLockMode() { - return Util.SDK_INT < 12 ? WifiManager.WIFI_MODE_FULL : WifiManager.WIFI_MODE_FULL_HIGH_PERF; - } - - private final class CheckCanStopRunnable implements Runnable { - - private static final long CHECK_INTERVAL_MS = 1000; - - private void startChecking() { - mainHandler.post(this); - } - - @Override - public void run() { - if (hostedTest.canStop()) { - hostedTestFinished = true; - maybeStopHostedTest(); - } else { - mainHandler.postDelayed(this, CHECK_INTERVAL_MS); - } - } - - } - }