From d3d63101d1b4266ae41c7341add5df33b23eec81 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Mar 2016 04:36:56 -0700 Subject: [PATCH] [Refactor - Step #5] Introduce TrackSelector Notes: - The way this works is that every time the player needs to select some tracks it invokes the TrackSelector. When a track selection is actually activated (i.e. "hits the screen") it gets passed back to the TrackSelector, which allows it to expose the current tracks through an API that it may choose to define. Since playlist support doesn't exist yet, it's currently the case that the pass-back always occurs immediately. - A TrackSelector can invalidate its previous selections if its selection criteria changes. This will force the player to invoke it again to make a new selection. If the new selection is the same as the previous one for a renderer then the player handles this efficiently (i.e. turns it into a no-op). - DefaultTrackSelector supports disabling/enabling of renderers. Separately, it supports overrides to select specific formats. Since formats may change (playlists/periods), overrides are specific to not only the renderer but also the set of formats that are available to it. If the formats available to a renderer change then the override will no longer apply. If the same set of formats become available at some point later, it will apply once more. This will nicely handle cases like ad-insertion where the ads have different formats, but all segments of main content use the same set of formats. - In general, in multi-period or playlist cases, the preferred way of selecting formats will be via constraints (e.g. "don't play HD", "prefer higher quality audio") rather than explicit format selections. The ability to set various constraints on DefaultTrackSelector is future work. Note about the demo app: - I've removed the verbose log toggle. I doubt anyone has ever used it! I've also removed the background audio option. Without using a service it can't be considered a reference implementation, so it's probably best to leave developers to figure this one out. Finally, listening to AudioCapabilities has also gone. This will be replaced by having the player detect and handle the capabilities change internally in a future CL. This will work by allowing a renderer to invalidate the track selections when its capabilities change, much like how a selector is able to invalidate the track selections in this CL. - It's now possible to enable ABR with an arbitrary subset of tracks. - Unsupported tracks are shown grayed out in the UI. I'm not showing tracks that aren't associated to any renderer, but we could optionally add that later. - Every time the tracks change, there's logcat output showing all of the tracks and which ones are enabled. Unassociated tracks are displayed in this output. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=117122202 --- .../android/exoplayer/demo/EventLogger.java | 153 ++++- .../exoplayer/demo/PlayerActivity.java | 209 +------ .../exoplayer/demo/player/DemoPlayer.java | 79 +-- .../demo/ui/TrackSelectionHelper.java | 277 +++++++++ demo/src/main/res/layout/list_divider.xml | 19 + demo/src/main/res/layout/player_activity.xml | 7 - .../res/layout/track_selection_dialog.xml | 19 + demo/src/main/res/values/strings.xml | 14 +- .../exoplayer/DefaultTrackSelector.java | 558 ++++++++++++++++++ .../google/android/exoplayer/ExoPlayer.java | 72 +-- .../android/exoplayer/ExoPlayerImpl.java | 47 +- .../exoplayer/ExoPlayerImplInternal.java | 242 ++++---- .../com/google/android/exoplayer/Format.java | 5 +- .../android/exoplayer/TrackGroupArray.java | 2 +- .../android/exoplayer/TrackSelection.java | 15 + .../exoplayer/TrackSelectionArray.java | 76 +++ .../android/exoplayer/TrackSelector.java | 77 +++ .../exoplayer/util/VerboseLogUtil.java | 81 --- 18 files changed, 1356 insertions(+), 596 deletions(-) create mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java create mode 100644 demo/src/main/res/layout/list_divider.xml create mode 100644 demo/src/main/res/layout/track_selection_dialog.xml create mode 100644 library/src/main/java/com/google/android/exoplayer/DefaultTrackSelector.java create mode 100644 library/src/main/java/com/google/android/exoplayer/TrackSelectionArray.java create mode 100644 library/src/main/java/com/google/android/exoplayer/TrackSelector.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/util/VerboseLogUtil.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java index 43681ca39c..0195cb12cb 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer.demo; +import com.google.android.exoplayer.DefaultTrackSelector.TrackInfo; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.TimeRange; +import com.google.android.exoplayer.TrackGroup; +import com.google.android.exoplayer.TrackGroupArray; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.demo.player.DemoPlayer; -import com.google.android.exoplayer.util.VerboseLogUtil; import android.media.MediaCodec.CryptoException; import android.os.SystemClock; @@ -46,13 +50,8 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } private long sessionStartTimeMs; - private long[] loadStartTimeMs; private long[] availableRangeValuesUs; - public EventLogger() { - loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT]; - } - public void startSession() { sessionStartTimeMs = SystemClock.elapsedRealtime(); Log.d(TAG, "start [0]"); @@ -82,6 +81,54 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener + ", " + pixelWidthHeightRatio + "]"); } + @Override + public void onTracksChanged(TrackInfo trackInfo) { + Log.d(TAG, "Tracks ["); + // Log tracks associated to renderers. + for (int rendererIndex = 0; rendererIndex < trackInfo.rendererCount; rendererIndex++) { + TrackGroupArray trackGroups = trackInfo.getTrackGroups(rendererIndex); + TrackSelection trackSelection = trackInfo.getTrackSelection(rendererIndex); + if (trackGroups.length > 0) { + Log.d(TAG, " Renderer:" + rendererIndex + " ["); + for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { + TrackGroup trackGroup = trackGroups.get(groupIndex); + String adaptiveSupport = getAdaptiveSupportString( + trackGroup.length, trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + String status = getTrackStatusString(trackSelection, groupIndex, trackIndex); + String formatSupport = getFormatSupportString( + trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); + Log.d(TAG, " " + status + " Track:" + trackIndex + ", " + + getFormatString(trackGroup.getFormat(trackIndex)) + + ", supported=" + formatSupport); + } + Log.d(TAG, " ]"); + } + Log.d(TAG, " ]"); + } + } + // Log tracks not associated with a renderer. + TrackGroupArray trackGroups = trackInfo.getUnassociatedTrackGroups(); + if (trackGroups.length > 0) { + Log.d(TAG, " Renderer:None ["); + for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { + Log.d(TAG, " Group:" + groupIndex + " ["); + TrackGroup trackGroup = trackGroups.get(groupIndex); + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + String status = getTrackStatusString(false); + String formatSupport = getFormatSupportString(TrackRenderer.FORMAT_UNSUPPORTED_TYPE); + Log.d(TAG, " " + status + " Track:" + trackIndex + ", " + + getFormatString(trackGroup.getFormat(trackIndex)) + + ", supported=" + formatSupport); + } + Log.d(TAG, " ]"); + } + Log.d(TAG, " ]"); + } + Log.d(TAG, "]"); + } + // DemoPlayer.InfoListener @Override @@ -98,21 +145,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener @Override public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, long mediaStartTimeMs, long mediaEndTimeMs) { - loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); - if (VerboseLogUtil.isTagEnabled(TAG)) { - Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type - + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]"); - } + // Do nothing. } @Override public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) { - if (VerboseLogUtil.isTagEnabled(TAG)) { - long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; - Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime - + "]"); - } + // Do nothing. } @Override @@ -187,7 +226,15 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } - private String getStateString(int state) { + private String getSessionTimeString() { + return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs); + } + + private static String getTimeString(long timeMs) { + return TIME_FORMAT.format((timeMs) / 1000f); + } + + private static String getStateString(int state) { switch (state) { case ExoPlayer.STATE_BUFFERING: return "B"; @@ -204,12 +251,76 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } } - private String getSessionTimeString() { - return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs); + private static String getFormatSupportString(int formatSupport) { + switch (formatSupport) { + case TrackRenderer.FORMAT_HANDLED: + return "YES"; + case TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES: + return "NO_EXCEEDS_CAPABILITIES"; + case TrackRenderer.FORMAT_UNSUPPORTED_SUBTYPE: + return "NO_UNSUPPORTED_TYPE"; + case TrackRenderer.FORMAT_UNSUPPORTED_TYPE: + return "NO"; + default: + return "?"; + } } - private String getTimeString(long timeMs) { - return TIME_FORMAT.format((timeMs) / 1000f); + private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { + if (trackCount < 2) { + return "N/A"; + } + switch (adaptiveSupport) { + case TrackRenderer.ADAPTIVE_SEAMLESS: + return "YES"; + case TrackRenderer.ADAPTIVE_NOT_SEAMLESS: + return "YES_NOT_SEAMLESS"; + case TrackRenderer.ADAPTIVE_NOT_SUPPORTED: + return "NO"; + default: + return "?"; + } + } + + private static String getFormatString(Format format) { + StringBuilder builder = new StringBuilder(); + builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType); + if (format.bitrate != Format.NO_VALUE) { + builder.append(", bitrate=").append(format.bitrate); + } + if (format.width != -1 && format.height != -1) { + builder.append(", res=").append(format.width).append("x").append(format.height); + } + if (format.frameRate != -1) { + builder.append(", fps=").append(format.frameRate); + } + if (format.channelCount != -1) { + builder.append(", channels=").append(format.channelCount); + } + if (format.sampleRate != -1) { + builder.append(", sample_rate=").append(format.sampleRate); + } + if (format.language != null) { + builder.append(", language=").append(format.language); + } + return builder.toString(); + } + + private static String getTrackStatusString(TrackSelection selection, int groupIndex, + int trackIndex) { + boolean groupEnabled = selection != null && selection.group == groupIndex; + if (groupEnabled) { + for (int i = 0; i < selection.length; i++) { + if (selection.getTrack(i) == trackIndex) { + return getTrackStatusString(true); + } + } + } + return getTrackStatusString(false); + } + + private static String getTrackStatusString(boolean enabled) { + return enabled ? "[X]" : "[ ]"; } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 8ada1a5d0e..6b5c7abfe3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -16,19 +16,19 @@ package com.google.android.exoplayer.demo; import com.google.android.exoplayer.AspectRatioFrameLayout; +import com.google.android.exoplayer.DefaultTrackSelector; +import com.google.android.exoplayer.DefaultTrackSelector.TrackInfo; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.Format; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; import com.google.android.exoplayer.demo.player.DashSourceBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer; import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; import com.google.android.exoplayer.demo.player.ExtractorSourceBuilder; import com.google.android.exoplayer.demo.player.HlsSourceBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingSourceBuilder; +import com.google.android.exoplayer.demo.ui.TrackSelectionHelper; import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.PrivMetadata; @@ -37,9 +37,7 @@ import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.SubtitleLayout; import com.google.android.exoplayer.util.DebugTextViewHelper; -import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; -import com.google.android.exoplayer.util.VerboseLogUtil; import android.Manifest.permission; import android.annotation.TargetApi; @@ -52,8 +50,6 @@ import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -64,8 +60,6 @@ import android.view.View.OnTouchListener; import android.view.accessibility.CaptioningManager; import android.widget.Button; import android.widget.MediaController; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; import android.widget.Toast; @@ -73,15 +67,13 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.List; -import java.util.Locale; import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. */ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener, - AudioCapabilitiesReceiver.Listener { + DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener { // For use within demo app code. public static final String CONTENT_ID_EXTRA = "content_id"; @@ -92,8 +84,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private static final String CONTENT_EXT_EXTRA = "type"; private static final String TAG = "PlayerActivity"; - private static final int MENU_GROUP_TRACKS = 1; - private static final int ID_OFFSET = 2; private static final CookieManager defaultCookieManager; static { @@ -116,19 +106,18 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private Button retryButton; private DemoPlayer player; + private DefaultTrackSelector trackSelector; + private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; private boolean playerNeedsPrepare; private long playerPosition; - private boolean enableBackgroundAudio; private Uri contentUri; private int contentType; private String contentId; private String provider; - private AudioCapabilitiesReceiver audioCapabilitiesReceiver; - // Activity lifecycle @Override @@ -182,9 +171,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (currentHandler != defaultCookieManager) { CookieHandler.setDefault(defaultCookieManager); } - - audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, this); - audioCapabilitiesReceiver.register(); } @Override @@ -204,31 +190,16 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, contentId = intent.getStringExtra(CONTENT_ID_EXTRA); provider = intent.getStringExtra(PROVIDER_EXTRA); configureSubtitleView(); - if (player == null) { - if (!maybeRequestPermission()) { - preparePlayer(true); - } - } else { - player.setBackgrounded(false); + if (!maybeRequestPermission()) { + preparePlayer(true); } } @Override public void onPause() { - super.onPause(); - if (!enableBackgroundAudio) { - releasePlayer(); - } else { - player.setBackgrounded(true); - } shutterView.setVisibility(View.VISIBLE); - } - - @Override - public void onDestroy() { - super.onDestroy(); - audioCapabilitiesReceiver.unregister(); releasePlayer(); + super.onPause(); } // OnClickListener methods @@ -240,20 +211,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, } } - // AudioCapabilitiesReceiver.Listener methods - - @Override - public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - if (player == null) { - return; - } - boolean backgrounded = player.getBackgrounded(); - boolean playWhenReady = player.getPlayWhenReady(); - releasePlayer(); - preparePlayer(playWhenReady); - player.setBackgrounded(backgrounded); - } - // Permission request listener method @Override @@ -321,6 +278,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, player.setCaptionListener(this); player.setMetadataListener(this); player.seekTo(playerPosition); + trackSelector = player.getTrackSelector(); + trackSelectionHelper = new TrackSelectionHelper(trackSelector); playerNeedsPrepare = true; mediaController.setMediaPlayer(player.getPlayerControl()); mediaController.setEnabled(true); @@ -355,6 +314,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, // DemoPlayer.Listener implementation + @Override + public void onTracksChanged(TrackInfo trackSet) { + updateButtonVisibilities(); + } + @Override public void onStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == ExoPlayer.STATE_ENDED) { @@ -440,142 +404,23 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, } private boolean haveTracks(int type) { - return player != null && player.getTrackCount(type) > 0; + TrackInfo trackInfo = player == null ? null : player.getTrackInfo(); + return trackInfo != null && trackInfo.getTrackGroups(type).length != 0; } - public void showVideoPopup(View v) { - PopupMenu popup = new PopupMenu(this, v); - configurePopupWithTracks(popup, null, DemoPlayer.TYPE_VIDEO); - popup.show(); + public void showVideoPopup(@SuppressWarnings("unused") View v) { + trackSelectionHelper.showSelectionDialog(this, R.string.video, player.getTrackInfo(), + DemoPlayer.TYPE_VIDEO); } - public void showAudioPopup(View v) { - PopupMenu popup = new PopupMenu(this, v); - Menu menu = popup.getMenu(); - menu.add(Menu.NONE, Menu.NONE, Menu.NONE, R.string.enable_background_audio); - final MenuItem backgroundAudioItem = menu.findItem(0); - backgroundAudioItem.setCheckable(true); - backgroundAudioItem.setChecked(enableBackgroundAudio); - OnMenuItemClickListener clickListener = new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item == backgroundAudioItem) { - enableBackgroundAudio = !item.isChecked(); - return true; - } - return false; - } - }; - configurePopupWithTracks(popup, clickListener, DemoPlayer.TYPE_AUDIO); - popup.show(); + public void showAudioPopup(@SuppressWarnings("unused") View v) { + trackSelectionHelper.showSelectionDialog(this, R.string.audio, player.getTrackInfo(), + DemoPlayer.TYPE_AUDIO); } - public void showTextPopup(View v) { - PopupMenu popup = new PopupMenu(this, v); - configurePopupWithTracks(popup, null, DemoPlayer.TYPE_TEXT); - popup.show(); - } - - public void showVerboseLogPopup(View v) { - PopupMenu popup = new PopupMenu(this, v); - Menu menu = popup.getMenu(); - menu.add(Menu.NONE, 0, Menu.NONE, R.string.logging_normal); - menu.add(Menu.NONE, 1, Menu.NONE, R.string.logging_verbose); - menu.setGroupCheckable(Menu.NONE, true, true); - menu.findItem((VerboseLogUtil.areAllTagsEnabled()) ? 1 : 0).setChecked(true); - popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == 0) { - VerboseLogUtil.setEnableAllTags(false); - } else { - VerboseLogUtil.setEnableAllTags(true); - } - return true; - } - }); - popup.show(); - } - - private void configurePopupWithTracks(PopupMenu popup, - final OnMenuItemClickListener customActionClickListener, - final int trackType) { - if (player == null) { - return; - } - int trackCount = player.getTrackCount(trackType); - if (trackCount == 0) { - return; - } - popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - return (customActionClickListener != null - && customActionClickListener.onMenuItemClick(item)) - || onTrackItemClick(item, trackType); - } - }); - Menu menu = popup.getMenu(); - // ID_OFFSET ensures we avoid clashing with Menu.NONE (which equals 0). - menu.add(MENU_GROUP_TRACKS, DemoPlayer.TRACK_DISABLED + ID_OFFSET, Menu.NONE, R.string.off); - for (int i = 0; i < trackCount; i++) { - menu.add(MENU_GROUP_TRACKS, i + ID_OFFSET, Menu.NONE, - buildTrackName(player.getTrackFormat(trackType, i))); - } - menu.setGroupCheckable(MENU_GROUP_TRACKS, true, true); - menu.findItem(player.getSelectedTrack(trackType) + ID_OFFSET).setChecked(true); - } - - private static String buildTrackName(Format format) { - String trackName; - if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), - buildBitrateString(format)), buildTrackIdString(format)); - } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildAudioPropertyString(format)), buildBitrateString(format)), - buildTrackIdString(format)); - } else { - trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)); - } - return trackName.length() == 0 ? "unknown" : trackName; - } - - private static String buildResolutionString(Format format) { - return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE - ? "" : format.width + "x" + format.height; - } - - private static String buildAudioPropertyString(Format format) { - return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE - ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; - } - - private static String buildLanguageString(Format format) { - return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" - : format.language; - } - - private static String buildBitrateString(Format format) { - return format.bitrate == Format.NO_VALUE ? "" - : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); - } - - private static String joinWithSeparator(String first, String second) { - return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); - } - - private static String buildTrackIdString(Format format) { - return format.id == null ? "" : " (" + format.id + ")"; - } - - private boolean onTrackItemClick(MenuItem item, int type) { - if (player == null || item.getGroupId() != MENU_GROUP_TRACKS) { - return false; - } - player.setSelectedTrack(type, item.getItemId() - ID_OFFSET); - return true; + public void showTextPopup(@SuppressWarnings("unused") View v) { + trackSelectionHelper.showSelectionDialog(this, R.string.text, player.getTrackInfo(), + DemoPlayer.TYPE_TEXT); } private void toggleControlsVisibility() { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 76e8be6348..3582ee9c13 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.DefaultTrackSelector; +import com.google.android.exoplayer.DefaultTrackSelector.TrackInfo; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.Format; @@ -51,7 +53,6 @@ import android.os.Handler; import android.view.Surface; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -61,11 +62,12 @@ import java.util.concurrent.CopyOnWriteArrayList; * with one of a number of {@link SourceBuilder} classes to suit different use cases (e.g. DASH, * SmoothStreaming and so on). */ -public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, - HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, - MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, - StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer, - MetadataRenderer>, DebugTextViewHelper.Provider { +public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.EventListener, + ChunkSampleSource.EventListener, HlsSampleSource.EventListener, + DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, + MediaCodecAudioTrackRenderer.EventListener, StreamingDrmSessionManager.EventListener, + DashChunkSource.EventListener, TextRenderer, MetadataRenderer>, + DebugTextViewHelper.Provider { /** * Builds a source to play. @@ -86,6 +88,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public interface Listener { void onStateChanged(boolean playWhenReady, int playbackState); void onError(Exception e); + void onTracksChanged(TrackInfo trackInfo); void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); } @@ -146,8 +149,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; public static final int STATE_READY = ExoPlayer.STATE_READY; public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; - public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED; - public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT; public static final int RENDERER_COUNT = 4; public static final int TYPE_VIDEO = 0; @@ -156,6 +157,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int TYPE_METADATA = 3; private final ExoPlayer player; + private final DefaultTrackSelector trackSelector; private final SourceBuilder sourceBuilder; private final BandwidthMeter bandwidthMeter; private final MediaCodecVideoTrackRenderer videoRenderer; @@ -165,9 +167,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private Surface surface; private Format videoFormat; - private int videoTrackToRestore; - - private boolean backgrounded; + private TrackInfo trackInfo; private CaptionListener captionListener; private Id3MetadataListener id3MetadataListener; @@ -193,12 +193,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi id3Renderer}; // Build the player and associated objects. - player = ExoPlayer.Factory.newInstance(renderers, 1000, 5000); + trackSelector = new DefaultTrackSelector(mainHandler, this); + player = ExoPlayer.Factory.newInstance(renderers, trackSelector, 1000, 5000); player.addListener(this); playerControl = new PlayerControl(player); + } - // Set initial state, with the text renderer initially disabled. - player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED); + public DefaultTrackSelector getTrackSelector() { + return trackSelector; } public PlayerControl getPlayerControl() { @@ -243,41 +245,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi pushSurface(true); } - public int getTrackCount(int type) { - return player.getTrackCount(type); - } - - public Format getTrackFormat(int type, int index) { - return player.getTrackFormat(type, index); - } - - public int getSelectedTrack(int type) { - return player.getSelectedTrack(type); - } - - public void setSelectedTrack(int type, int index) { - player.setSelectedTrack(type, index); - if (type == TYPE_TEXT && index < 0 && captionListener != null) { - captionListener.onCues(Collections.emptyList()); - } - } - - public boolean getBackgrounded() { - return backgrounded; - } - - public void setBackgrounded(boolean backgrounded) { - if (this.backgrounded == backgrounded) { - return; - } - this.backgrounded = backgrounded; - if (backgrounded) { - videoTrackToRestore = getSelectedTrack(TYPE_VIDEO); - setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED); - blockingClearSurface(); - } else { - setSelectedTrack(TYPE_VIDEO, videoTrackToRestore); - } + public TrackInfo getTrackInfo() { + return trackInfo; } public void prepare() { @@ -351,6 +320,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } + @Override + public void onTracksChanged(TrackInfo trackInfo) { + this.trackInfo = trackInfo; + for (Listener listener : listeners) { + listener.onTracksChanged(trackInfo); + } + } + @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { @@ -451,14 +428,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public void onCues(List cues) { - if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) { + if (captionListener != null && trackInfo.getTrackSelection(TYPE_TEXT) != null) { captionListener.onCues(cues); } } @Override public void onMetadata(Map metadata) { - if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) { + if (id3MetadataListener != null && trackInfo.getTrackSelection(TYPE_METADATA) != null) { id3MetadataListener.onId3Metadata(metadata); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java new file mode 100644 index 0000000000..f96749af4d --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2014 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.exoplayer.demo.ui; + +import com.google.android.exoplayer.DefaultTrackSelector; +import com.google.android.exoplayer.DefaultTrackSelector.TrackInfo; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.TrackGroup; +import com.google.android.exoplayer.TrackGroupArray; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.TrackSelection; +import com.google.android.exoplayer.demo.R; +import com.google.android.exoplayer.util.MimeTypes; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.text.TextUtils; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckedTextView; + +import java.util.Arrays; +import java.util.Locale; + +/** + * Helper class for displaying track selection dialogs. + */ +public class TrackSelectionHelper implements View.OnClickListener, DialogInterface.OnClickListener { + + private final DefaultTrackSelector selector; + + private CheckedTextView disableView; + private CheckedTextView defaultView; + private CheckedTextView[][] trackViews; + + private TrackInfo trackInfo; + private int rendererIndex; + private TrackGroupArray trackGroups; + private boolean[] trackGroupsAdaptive; + + private boolean isDisabled; + private TrackSelection override; + + /** + * @param selector The track selector. + */ + public TrackSelectionHelper(DefaultTrackSelector selector) { + this.selector = selector; + } + + /** + * Shows the selection dialog for a given renderer. + * + * @param activity The parent activity. + * @param titleId The dialog's title. + * @param trackInfo The current track information. + * @param rendererIndex The index of the renderer. + */ + public void showSelectionDialog(Activity activity, int titleId, TrackInfo trackInfo, + int rendererIndex) { + this.trackInfo = trackInfo; + this.rendererIndex = rendererIndex; + + trackGroups = trackInfo.getTrackGroups(rendererIndex); + trackGroupsAdaptive = new boolean[trackGroups.length]; + for (int i = 0; i < trackGroups.length; i++) { + trackGroupsAdaptive[i] = trackInfo.getAdaptiveSupport(rendererIndex, i, false) + != TrackRenderer.ADAPTIVE_NOT_SUPPORTED; + } + isDisabled = selector.getRendererDisabled(rendererIndex); + if (selector.hasSelectionOverride(rendererIndex, trackGroups)) { + override = trackInfo.getTrackSelection(rendererIndex); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(titleId) + .setView(buildView(LayoutInflater.from(builder.getContext()))) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show(); + } + + @SuppressLint("InflateParams") + private View buildView(LayoutInflater inflater) { + ViewGroup root = (ViewGroup) inflater.inflate(R.layout.track_selection_dialog, null); + + // View for disabling the renderer. + disableView = (CheckedTextView) inflater.inflate( + android.R.layout.simple_list_item_single_choice, root, false); + disableView.setText(R.string.selection_disabled); + disableView.setOnClickListener(this); + root.addView(disableView); + + // View for clearing the override to allow the selector to use its default selection logic. + defaultView = (CheckedTextView) inflater.inflate( + android.R.layout.simple_list_item_single_choice, root, false); + defaultView.setText(R.string.selection_default); + defaultView.setOnClickListener(this); + root.addView(inflater.inflate(R.layout.list_divider, root, false)); + root.addView(defaultView); + + // Per-track views. + boolean haveSupportedTracks = false; + trackViews = new CheckedTextView[trackGroups.length][]; + for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { + root.addView(inflater.inflate(R.layout.list_divider, root, false)); + TrackGroup group = trackGroups.get(groupIndex); + trackViews[groupIndex] = new CheckedTextView[group.length]; + for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + int trackViewLayoutId = group.length < 2 || !trackGroupsAdaptive[groupIndex] + ? android.R.layout.simple_list_item_single_choice + : android.R.layout.simple_list_item_multiple_choice; + CheckedTextView trackView = (CheckedTextView) inflater.inflate( + trackViewLayoutId, root, false); + trackView.setText(buildTrackName(group.getFormat(trackIndex))); + if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) + == TrackRenderer.FORMAT_HANDLED) { + haveSupportedTracks = true; + trackView.setTag(Pair.create(groupIndex, trackIndex)); + trackView.setOnClickListener(this); + } else { + trackView.setEnabled(false); + } + trackViews[groupIndex][trackIndex] = trackView; + root.addView(trackView); + } + } + + if (!haveSupportedTracks) { + // Indicate that the default selection will be nothing. + defaultView.setText(R.string.selection_default_none); + } + + updateViews(); + return root; + } + + private void updateViews() { + disableView.setChecked(isDisabled); + defaultView.setChecked(!isDisabled && override == null); + for (int i = 0; i < trackViews.length; i++) { + for (int j = 0; j < trackViews[i].length; j++) { + trackViews[i][j].setChecked( + override != null && override.group == i && override.containsTrack(j)); + } + } + } + + // DialogInterface.OnClickListener + + @Override + public void onClick(DialogInterface dialog, int which) { + if (isDisabled) { + selector.setRendererDisabled(rendererIndex, true); + return; + } + selector.setRendererDisabled(rendererIndex, false); + if (override != null) { + selector.setSelectionOverride(rendererIndex, trackGroups, override); + } else { + selector.clearSelectionOverrides(rendererIndex); + } + } + + // View.OnClickListener + + @Override + public void onClick(View view) { + if (view == disableView) { + isDisabled = true; + override = null; + } else if (view == defaultView) { + isDisabled = false; + override = null; + } else { + isDisabled = false; + @SuppressWarnings("unchecked") + Pair tag = (Pair) view.getTag(); + int groupIndex = tag.first; + int trackIndex = tag.second; + if (!trackGroupsAdaptive[groupIndex] || override == null) { + override = new TrackSelection(groupIndex, trackIndex); + } else { + // The group being modified is adaptive and we already have a non-null override. + boolean isEnabled = ((CheckedTextView) view).isChecked(); + if (isEnabled) { + // Remove the track from the override. + if (override.length == 1) { + // The last track is being removed, so the override becomes empty. + override = null; + } else { + int[] tracks = new int[override.length - 1]; + int trackCount = 0; + for (int i = 0; i < override.length; i++) { + if (override.getTrack(i) != trackIndex) { + tracks[trackCount++] = override.getTrack(i); + } + } + override = new TrackSelection(groupIndex, tracks); + } + } else { + // Add the track to the override. + int[] tracks = Arrays.copyOf(override.getTracks(), override.length + 1); + tracks[tracks.length - 1] = trackIndex; + override = new TrackSelection(groupIndex, tracks); + } + } + } + // Update the views with the new state. + updateViews(); + } + + // Track name construction. + + private static String buildTrackName(Format format) { + String trackName; + if (MimeTypes.isVideo(format.sampleMimeType)) { + trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), + buildBitrateString(format)), buildTrackIdString(format)); + } else if (MimeTypes.isAudio(format.sampleMimeType)) { + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildAudioPropertyString(format)), buildBitrateString(format)), + buildTrackIdString(format)); + } else { + trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildBitrateString(format)), buildTrackIdString(format)); + } + return trackName.length() == 0 ? "unknown" : trackName; + } + + private static String buildResolutionString(Format format) { + return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE + ? "" : format.width + "x" + format.height; + } + + private static String buildAudioPropertyString(Format format) { + return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE + ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; + } + + private static String buildLanguageString(Format format) { + return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" + : format.language; + } + + private static String buildBitrateString(Format format) { + return format.bitrate == Format.NO_VALUE ? "" + : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); + } + + private static String joinWithSeparator(String first, String second) { + return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); + } + + private static String buildTrackIdString(Format format) { + return format.id == null ? "" : " (" + format.id + ")"; + } + +} diff --git a/demo/src/main/res/layout/list_divider.xml b/demo/src/main/res/layout/list_divider.xml new file mode 100644 index 0000000000..21b2feb5ea --- /dev/null +++ b/demo/src/main/res/layout/list_divider.xml @@ -0,0 +1,19 @@ + + + diff --git a/demo/src/main/res/layout/player_activity.xml b/demo/src/main/res/layout/player_activity.xml index ae0aa5f1d2..4e189292c1 100644 --- a/demo/src/main/res/layout/player_activity.xml +++ b/demo/src/main/res/layout/player_activity.xml @@ -94,13 +94,6 @@ android:visibility="gone" android:onClick="showTextPopup"/> -