diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d949570d7..f9a03390ef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,130 @@ # Release notes # +### 2.7.0 ### + +* Player interface: + * Add optional parameter to `stop` to reset the player when stopping. + * Add a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. + * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons + reported in `Eventlistener.onPositionDiscontinuity` to distinguish + transitions to and from ads within one period from transitions between + periods. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. `SeekParameters` are not currently supported when playing HLS + streams. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. +* Buffering: + * Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured by + custom `LoadControl` implementations. + * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. + * Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. + * Play out existing buffer before retrying for progressive live streams + ([#1606](https://github.com/google/ExoPlayer/issues/1606)). +* UI components: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. + * Made `PlayerView` automatically apply video rotation when configured to use + `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). + * Made `PlayerView` play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data in the manifest. If playback of clear + samples without keys is allowed, delay DRM session error propagation until + keys are actually needed + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). +* DASH: + * Support in-band Emsg events targeting the player with scheme id + "urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3". + * Support EventStream elements in DASH manifests. +* HLS: + * Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details + can be found + [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6). + * Fail if unable to sync with the Transport Stream, rather than entering + stuck in an indefinite buffering state. + * Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). + * Fix ID3 context reuse across segment format changes + ([#3622](https://github.com/google/ExoPlayer/issues/3622)). + * Use long for media sequence numbers + ([#3747](https://github.com/google/ExoPlayer/issues/3747)) + * Add initial support for the EXT-X-GAP tag. +* Audio: + * Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). + * Support resampling 24-bit and 32-bit integer to 32-bit float for high + resolution output in `DefaultAudioSink` + ([#3635](https://github.com/google/ExoPlayer/pull/3635)). +* Captions: + * Basic support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). + * Fix handling of CEA-608 captions where multiple buffers have the same + presentation timestamp + ([#3782](https://github.com/google/ExoPlayer/issues/3782)). +* Caching: + * Fix cache corruption issue + ([#3762](https://github.com/google/ExoPlayer/issues/3762)). + * Implement periodic check in `CacheDataSource` to see whether it's possible + to switch to reading/writing the cache having initially bypassed it. +* IMA extension: + * Fix the player getting stuck when an ad group fails to load + ([#3584](https://github.com/google/ExoPlayer/issues/3584)). + * Work around loadAd not being called beore the LOADED AdEvent arrives + ([#3552](https://github.com/google/ExoPlayer/issues/3552)). + * Handle asset mismatch errors + ([#3801](https://github.com/google/ExoPlayer/issues/3801)). + * Add support for playing non-Extractor content MediaSources in + the IMA demo app + ([#3676](https://github.com/google/ExoPlayer/issues/3676)). + * Fix handling of ad tags where ad groups are out of order + ([#3716](https://github.com/google/ExoPlayer/issues/3716)). + * Fix handling of ad tags with only preroll/postroll ad groups + ([#3715](https://github.com/google/ExoPlayer/issues/3715)). + * Propagate ad media preparation errors to IMA so that the ads can be + skipped. + * Handle exceptions in IMA callbacks so that can be logged less verbosely. +* New Cast extension. Simplifies toggling between local and Cast playbacks. +* `EventLogger` moved from the demo app into the core library. +* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, + Lenovo K4 Note and Sony Xperia E5. + ([#3724](https://github.com/google/ExoPlayer/issues/3724), + [#3835](https://github.com/google/ExoPlayer/issues/3835)). +* Fix potential NPE when removing media sources from a + DynamicConcatenatingMediaSource + ([#3796](https://github.com/google/ExoPlayer/issues/3796)). +* Check `sys.display-size` on Philips ATVs + ([#3807](https://github.com/google/ExoPlayer/issues/3807)). +* Release `Extractor`s on the loading thread to avoid potentially leaking + resources when the playback thread has quit by the time the loading task has + completed. +* ID3: Better handle malformed ID3 data + ([#3792](https://github.com/google/ExoPlayer/issues/3792). +* Support 14-bit mode and little endianness in DTS PES packets + ([#3340](https://github.com/google/ExoPlayer/issues/3340)). + ### 2.6.1 ### * Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, @@ -41,6 +166,8 @@ ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Prevent period transitions when seeking to the end of a period when paused + ([#2439](https://github.com/google/ExoPlayer/issues/2439)). ### 2.6.0 ### diff --git a/constants.gradle b/constants.gradle index c18fb28d4d..b02e2d4c37 100644 --- a/constants.gradle +++ b/constants.gradle @@ -26,9 +26,9 @@ project.ext { dexmakerVersion = '1.2' mockitoVersion = '1.9.5' junitVersion = '4.12' - truthVersion = '0.35' - robolectricVersion = '3.4.2' - releaseVersion = '2.6.1' + truthVersion = '0.39' + robolectricVersion = '3.7.1' + releaseVersion = '2.7.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 7a8320b1a1..20a7c87bde 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -28,6 +28,7 @@ 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' @@ -46,6 +47,7 @@ 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') 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..8f074c9238 --- /dev/null +++ b/demos/cast/build.gradle @@ -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. +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') + compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion +} diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..d23576572a --- /dev/null +++ b/demos/cast/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java new file mode 100644 index 0000000000..26ab5eb0dd --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.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 DemoUtil { + + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; + public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; + public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; + 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 mimeType; + + /** + * @param uri See {@link #uri}. + * @param name See {@link #name}. + * @param mimeType See {@link #mimeType}. + */ + public Sample(String uri, String name, String mimeType) { + this.uri = uri; + this.name = name; + this.mimeType = mimeType; + } + + @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 DemoUtil() {} + +} 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..3e48ab2ab4 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -0,0 +1,261 @@ +/* + * 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.os.Bundle; +import android.support.v4.graphics.ColorUtils; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; + +/** + * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. + */ +public class MainActivity extends AppCompatActivity implements OnClickListener, + PlayerManager.QueuePositionListener { + + private PlayerView localPlayerView; + private PlayerControlView castControlView; + private PlayerManager playerManager; + private RecyclerView mediaQueueList; + private MediaQueueListAdapter mediaQueueListAdapter; + private CastContext castContext; + + // Activity lifecycle methods. + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + castContext = CastContext.getSharedInstance(this); + + setContentView(R.layout.main_activity); + + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); + + castControlView = findViewById(R.id.cast_control_view); + + mediaQueueList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(mediaQueueList); + mediaQueueList.setLayoutManager(new LinearLayoutManager(this)); + mediaQueueList.setHasFixedSize(true); + mediaQueueListAdapter = new MediaQueueListAdapter(); + + findViewById(R.id.add_sample_button).setOnClickListener(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu, menu); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); + return true; + } + + @Override + public void onResume() { + super.onResume(); + playerManager = + PlayerManager.createPlayerManager( + /* queuePositionListener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); + mediaQueueList.setAdapter(mediaQueueListAdapter); + } + + @Override + public void onPause() { + super.onPause(); + mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); + mediaQueueList.setAdapter(null); + playerManager.release(); + } + + // 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); + } + + @Override + public void onClick(View view) { + new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) + .setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() + .show(); + } + + // PlayerManager.QueuePositionListener implementation. + + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(newIndex); + } + } + + // Internal methods. + + private View buildSampleListView() { + View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); + ListView sampleList = dialogList.findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter(this)); + sampleList.setOnItemClickListener( + new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + } + }); + return dialogList; + } + + // Internal classes. + + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); + } + + } + + private class MediaQueueListAdapter extends RecyclerView.Adapter { + + @Override + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + TextView view = holder.textView; + view.setText(playerManager.getItem(position).name); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; + } + draggingToPosition = toPosition; + mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + if (playerManager.removeItem(position)) { + mediaQueueListAdapter.notifyItemRemoved(position); + } + } + + @Override + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + mediaQueueListAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); + } + + } + +} 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..ac488ff3fd --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -0,0 +1,425 @@ +/* + * 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.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.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.castdemo.DemoUtil.Sample; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +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.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; + +/** + * Manages players and an internal media queue for the ExoPlayer/Cast demo app. + */ +/* package */ final class PlayerManager extends DefaultEventListener + implements CastPlayer.SessionAvailabilityListener { + + /** + * Listener for changes in the media queue playback position. + */ + public interface QueuePositionListener { + + /** + * Called when the currently played item of the media queue changes. + */ + void onQueuePositionChanged(int previousIndex, int newIndex); + + } + + 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 PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final QueuePositionListener queuePositionListener; + + private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; + private boolean castMediaQueueCreationPending; + private int currentItemIndex; + private Player currentPlayer; + + /** + * @param queuePositionListener A {@link QueuePositionListener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. + */ + public static PlayerManager createPlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + PlayerManager playerManager = + new PlayerManager( + queuePositionListener, localPlayerView, castControlView, context, castContext); + playerManager.init(); + return playerManager; + } + + private PlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.queuePositionListener = queuePositionListener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + + DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); + exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); + + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); + } + + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** + * Returns the index of the currently played item. + */ + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code sample} to the media queue. + * + * @param sample The {@link Sample} to append. + */ + public void addItem(Sample sample) { + mediaQueue.add(sample); + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + } else { + castPlayer.addItems(buildMediaQueueItem(sample)); + } + } + + /** + * Returns the size of the media queue. + */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public Sample getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param itemIndex The index of the item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(int itemIndex) { + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); + } else { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param fromIndex The index of the item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(int fromIndex, int toIndex) { + // Player update. + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + // Miscellaneous methods. + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** + * Releases the manager and the players that it holds. + */ + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + updateCurrentItemIndex(); + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(castPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void init() { + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + if (this.currentPlayer != null) { + int playbackState = this.currentPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = this.currentPlayer.getCurrentPosition(); + playWhenReady = this.currentPlayer.getPlayWhenReady(); + windowIndex = this.currentPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + this.currentPlayer.stop(true); + } else { + // This is the initial setup. No need to save any state. + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + castMediaQueueCreationPending = currentPlayer == castPlayer; + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); + for (int i = 0; i < mediaQueue.size(); i++) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); + } + exoPlayer.prepare(dynamicConcatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (castMediaQueueCreationPending) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castMediaQueueCreationPending = false; + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private static MediaSource buildMediaSource(DemoUtil.Sample sample) { + Uri uri = Uri.parse(sample.uri); + switch (sample.mimeType) { + case DemoUtil.MIME_TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); + case DemoUtil.MIME_TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); + case DemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_VIDEO_MP4: + return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + default: { + throw new IllegalStateException("Unsupported type: " + sample.mimeType); + } + } + } + + private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); + MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) + .setMetadata(movieMetadata).build(); + return new MediaQueueItem.Builder(mediaInfo).build(); + } + +} diff --git a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml new file mode 100644 index 0000000000..5f3c8961ef --- /dev/null +++ b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml @@ -0,0 +1,20 @@ + + + + + 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..01e48cdea7 --- /dev/null +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/demos/cast/src/main/res/layout/sample_list.xml b/demos/cast/src/main/res/layout/sample_list.xml new file mode 100644 index 0000000000..910db9e058 --- /dev/null +++ b/demos/cast/src/main/res/layout/sample_list.xml @@ -0,0 +1,25 @@ + + + + + + + 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..3505c40400 --- /dev/null +++ b/demos/cast/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + + + Exo Cast Demo + + Cast + + Add samples + + diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 536d8d4662..5225c260f8 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -45,5 +45,6 @@ dependencies { compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'library-dash') compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 0efeaf6f7f..7f169b8095 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2700" + android:versionName="2.7.0"> diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java index fb9cd05cd4..9988108f32 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.imademo; import android.app.Activity; import android.os.Bundle; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerView; /** * Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.ui.SimpleExoPlayerView; */ public final class MainActivity extends Activity { - private SimpleExoPlayerView playerView; + private PlayerView playerView; private PlayerManager player; @Override diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 51959451d1..e51c5e89b7 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -32,11 +32,13 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; @@ -66,7 +68,7 @@ import com.google.android.exoplayer2.util.Util; new DefaultBandwidthMeter()); } - public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { + public void init(Context context, PlayerView playerView) { // Create a default track selector. BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); TrackSelection.Factory videoTrackSelectionFactory = @@ -77,17 +79,12 @@ import com.google.android.exoplayer2.util.Util; player = ExoPlayerFactory.newSimpleInstance(context, trackSelector); // Bind the player to the view. - simpleExoPlayerView.setPlayer(player); - - // Produces DataSource instances through which media data is loaded. - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, - Util.getUserAgent(context, context.getString(R.string.application_name))); + playerView.setPlayer(player); // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Factory(dataSourceFactory) - .createMediaSource(Uri.parse(contentUrl)); + buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = @@ -95,7 +92,7 @@ import com.google.android.exoplayer2.util.Util; contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, - simpleExoPlayerView.getOverlayFrameLayout(), + playerView.getOverlayFrameLayout(), /* eventHandler= */ null, /* eventListener= */ null); @@ -126,6 +123,19 @@ import com.google.android.exoplayer2.util.Util; @Override public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return buildMediaSource(uri, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + // IMA does not support Smooth Streaming ads. + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } + + // Internal methods. + + private MediaSource buildMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { @ContentType int type = Util.inferContentType(uri); switch (type) { case C.TYPE_DASH: @@ -133,20 +143,19 @@ import com.google.android.exoplayer2.util.Util; new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, handler, listener); - case C.TYPE_SS: default: throw new IllegalStateException("Unsupported type: " + type); } } - @Override - public int[] getSupportedTypes() { - return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; - } } diff --git a/demos/ima/src/main/res/layout/main_activity.xml b/demos/ima/src/main/res/layout/main_activity.xml index ad5da62f47..f7ea5c9b88 100644 --- a/demos/ima/src/main/res/layout/main_activity.xml +++ b/demos/ima/src/main/res/layout/main_activity.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -(android.content.Context, android.net.Uri); +} diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 00326157a2..a98176d93b 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2700" + android:versionName="2.7.0"> diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 38a0c577ae..7052e7c436 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", @@ -566,6 +566,16 @@ "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + }, + { + "name": "VMAP empty midroll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll" + }, + { + "name": "VMAP full, empty, full midrolls", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2" } ] } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index b5db4c018d..5d019e4c53 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.demo; import android.app.Application; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Util; /** @@ -36,13 +36,15 @@ public class DemoApplication extends Application { userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); } - public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { - return new DefaultDataSourceFactory(this, bandwidthMeter, - buildHttpDataSourceFactory(bandwidthMeter)); + /** Returns a {@link DataSource.Factory}. */ + public DataSource.Factory buildDataSourceFactory(TransferListener listener) { + return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); } - public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { - return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); + /** Returns a {@link HttpDataSource.Factory}. */ + public HttpDataSource.Factory buildHttpDataSourceFactory( + TransferListener listener) { + return new DefaultHttpDataSourceFactory(userAgent, listener); } public boolean useExtensionRenderers() { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index 5ff7c5cb40..2692bc4531 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -16,44 +16,15 @@ package com.google.android.exoplayer2.demo; import android.text.TextUtils; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; import java.util.Locale; -import java.util.UUID; /** * Utility methods for demo application. */ /* package */ final class DemoUtil { - /** - * Derives a DRM {@link UUID} from {@code drmScheme}. - * - * @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or - * {@code "clearkey"}. - * @return The derived {@link UUID}. - * @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}. - */ - public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException { - switch (Util.toLowerInvariant(drmScheme)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "clearkey": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(drmScheme); - } catch (RuntimeException e) { - throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); - } - } - } - /** * Builds a track name for display. * diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index fa3c7d401a..e91feaa291 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.demo; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -39,6 +38,7 @@ import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; @@ -68,22 +68,22 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.DebugTextViewHelper; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; +import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.UUID; -/** - * An activity that plays media using {@link SimpleExoPlayer}. - */ -public class PlayerActivity extends Activity implements OnClickListener, - PlaybackControlView.VisibilityListener { +/** An activity that plays media using {@link SimpleExoPlayer}. */ +public class PlayerActivity extends Activity + implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL = "drm_license_url"; @@ -112,10 +112,9 @@ public class PlayerActivity extends Activity implements OnClickListener, private Handler mainHandler; private EventLogger eventLogger; - private SimpleExoPlayerView simpleExoPlayerView; + private PlayerView playerView; private LinearLayout debugRootView; private TextView debugTextView; - private Button retryButton; private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; @@ -153,12 +152,10 @@ public class PlayerActivity extends Activity implements OnClickListener, rootView.setOnClickListener(this); debugRootView = findViewById(R.id.controls_root); debugTextView = findViewById(R.id.debug_text_view); - retryButton = findViewById(R.id.retry_button); - retryButton.setOnClickListener(this); - simpleExoPlayerView = findViewById(R.id.player_view); - simpleExoPlayerView.setControllerVisibilityListener(this); - simpleExoPlayerView.requestFocus(); + playerView = findViewById(R.id.player_view); + playerView.setControllerVisibilityListener(this); + playerView.requestFocus(); } @Override @@ -223,16 +220,14 @@ public class PlayerActivity extends Activity implements OnClickListener, @Override public boolean dispatchKeyEvent(KeyEvent event) { // See whether the player view wants to handle media or DPAD keys events. - return simpleExoPlayerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); + return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); } // OnClickListener methods @Override public void onClick(View view) { - if (view == retryButton) { - initializePlayer(); - } else if (view.getParent() == debugRootView) { + if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { trackSelectionHelper.showSelectionDialog( @@ -241,6 +236,13 @@ public class PlayerActivity extends Activity implements OnClickListener, } } + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + initializePlayer(); + } + // PlaybackControlView.VisibilityListener implementation @Override @@ -273,9 +275,14 @@ public class PlayerActivity extends Activity implements OnClickListener, try { String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA : DRM_SCHEME_UUID_EXTRA; - UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); - drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray, multiSession); + UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); + if (drmSchemeUuid == null) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + drmSessionManager = + buildDrmSessionManagerV18( + drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); + } } catch (UnsupportedDrmException e) { errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; @@ -302,9 +309,10 @@ public class PlayerActivity extends Activity implements OnClickListener, player.addMetadataOutput(eventLogger); player.addAudioDebugListener(eventLogger); player.addVideoDebugListener(eventLogger); - - simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); + + playerView.setPlayer(player); + playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } @@ -345,9 +353,10 @@ public class PlayerActivity extends Activity implements OnClickListener, releaseAdsLoader(); loadedAdTagUri = adTagUri; } - try { - mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - } catch (Exception e) { + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); + if (adsMediaSource != null) { + mediaSource = adsMediaSource; + } else { showToast(R.string.ima_not_loaded); } } else { @@ -455,39 +464,47 @@ public class PlayerActivity extends Activity implements OnClickListener, .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); } - /** - * Returns an ads media source, reusing the ads loader if one exists. - * - * @throws Exception Thrown if it was not possible to create an ads media source, for example, due - * to a missing dependency. - */ - private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception { + /** Returns an ads media source, reusing the ads loader if one exists. */ + private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { // Load the extension source using reflection so the demo app doesn't have to depend on it. // The ads loader is reused for multiple playbacks, so that ad playback can resume. - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (adsLoader == null) { - adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class) - .newInstance(this, adTagUri); - adUiViewGroup = new FrameLayout(this); - // The demo app has a non-null overlay frame layout. - simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); - } - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { - @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return PlayerActivity.this.buildMediaSource( - uri, /* overrideExtension= */ null, handler, listener); - } + try { + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + if (adsLoader == null) { + // Full class names used so the LINT.IfChange rule triggers should any of the classes move. + // LINT.IfChange + Constructor loaderConstructor = + loaderClass + .asSubclass(AdsLoader.class) + .getConstructor(android.content.Context.class, android.net.Uri.class); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + adsLoader = loaderConstructor.newInstance(this, adTagUri); + adUiViewGroup = new FrameLayout(this); + // The demo app has a non-null overlay frame layout. + playerView.getOverlayFrameLayout().addView(adUiViewGroup); + } + AdsMediaSource.MediaSourceFactory adMediaSourceFactory = + new AdsMediaSource.MediaSourceFactory() { + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return PlayerActivity.this.buildMediaSource( + uri, /* overrideExtension= */ null, handler, listener); + } - @Override - public int[] getSupportedTypes() { - return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; - } - }; - return new AdsMediaSource( - mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource( + mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); + } catch (ClassNotFoundException e) { + // IMA extension not loaded. + return null; + } catch (Exception e) { + throw new RuntimeException(e); + } } private void releaseAdsLoader() { @@ -495,7 +512,7 @@ public class PlayerActivity extends Activity implements OnClickListener, adsLoader.release(); adsLoader = null; loadedAdTagUri = null; - simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); + playerView.getOverlayFrameLayout().removeAllViews(); } } @@ -503,10 +520,6 @@ public class PlayerActivity extends Activity implements OnClickListener, private void updateButtonVisibilities() { debugRootView.removeAllViews(); - - retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); - debugRootView.addView(retryButton); - if (player == null) { return; } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 308bab2a3b..3895ad8e84 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -33,7 +33,6 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -202,11 +201,9 @@ public class SampleChooserActivity extends Activity { break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - try { - drmUuid = DemoUtil.getDrmUuid(reader.nextString()); - } catch (UnsupportedDrmException e) { - throw new ParserException(e); - } + String drmScheme = reader.nextString(); + drmUuid = Util.getDrmUuid(drmScheme); + Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme); break; case "drm_license_url": Assertions.checkState(!insidePlaylist, diff --git a/demos/main/src/main/proguard-rules.txt b/demos/main/src/main/proguard-rules.txt new file mode 100644 index 0000000000..cd201892ab --- /dev/null +++ b/demos/main/src/main/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the main demo app. + +# Constructor accessed via reflection in PlayerActivity +-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader +-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader { + (android.content.Context, android.net.Uri); +} diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml index 3f8cdaa7d6..6b84033273 100644 --- a/demos/main/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -20,7 +20,7 @@ android:layout_height="match_parent" android:keepScreenOn="true"> - @@ -42,15 +42,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="gone"> - -