diff options
| -rw-r--r-- | api/current.txt | 3 | ||||
| -rw-r--r-- | media/java/android/media/MediaFormat.java | 36 | ||||
| -rw-r--r-- | media/java/android/media/MediaPlayer.java | 49 | ||||
| -rw-r--r-- | media/java/android/media/SubtitleController.java | 179 | ||||
| -rw-r--r-- | media/java/android/media/SubtitleTrack.java | 6 |
5 files changed, 209 insertions, 64 deletions
diff --git a/api/current.txt b/api/current.txt index c7bb3b9c6971..3b0dce179347 100644 --- a/api/current.txt +++ b/api/current.txt @@ -12757,6 +12757,9 @@ package android.media { field public static final java.lang.String KEY_FRAME_RATE = "frame-rate"; field public static final java.lang.String KEY_HEIGHT = "height"; field public static final java.lang.String KEY_IS_ADTS = "is-adts"; + field public static final java.lang.String KEY_IS_AUTOSELECT = "is-autoselect"; + field public static final java.lang.String KEY_IS_DEFAULT = "is-default"; + field public static final java.lang.String KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle"; field public static final java.lang.String KEY_I_FRAME_INTERVAL = "i-frame-interval"; field public static final java.lang.String KEY_LANGUAGE = "language"; field public static final java.lang.String KEY_MAX_HEIGHT = "max-height"; diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 16ae43d02d7d..1ae890132f97 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -222,26 +222,36 @@ public final class MediaFormat { public static final String KEY_FLAC_COMPRESSION_LEVEL = "flac-compression-level"; /** - * A key for boolean AUTOSELECT field. Tracks with AUTOSELECT=true are - * considered when automatically selecting a track without specific user - * choice (as defined by HLS). - * @hide + * A key for boolean AUTOSELECT behavior for the track. Tracks with AUTOSELECT=true + * are considered when automatically selecting a track without specific user + * choice, based on the current locale. + * This is currently only used for subtitle tracks, when the user selected + * 'Default' for the captioning locale. + * The associated value is an integer, where non-0 means TRUE. This is an optional + * field; if not specified, AUTOSELECT defaults to TRUE. */ - public static final String KEY_AUTOSELECT = "autoselect"; + public static final String KEY_IS_AUTOSELECT = "is-autoselect"; /** - * A key for boolean DEFAULT field. The track with DEFAULT=true is selected - * in the absence of a specific user choice (as defined by HLS). - * @hide + * A key for boolean DEFAULT behavior for the track. The track with DEFAULT=true is + * selected in the absence of a specific user choice. + * This is currently only used for subtitle tracks, when the user selected + * 'Default' for the captioning locale. + * The associated value is an integer, where non-0 means TRUE. This is an optional + * field; if not specified, DEFAULT is considered to be FALSE. */ - public static final String KEY_DEFAULT = "default"; + public static final String KEY_IS_DEFAULT = "is-default"; + /** - * A key for boolean FORCED field for subtitle tracks. True if it is a - * forced subtitle track. - * @hide + * A key for the FORCED field for subtitle tracks. True if it is a + * forced subtitle track. Forced subtitle tracks are essential for the + * content and are shown even when the user turns off Captions. They + * are used for example to translate foreign/alien dialogs or signs. + * The associated value is an integer, where non-0 means TRUE. This is an + * optional field; if not specified, FORCED defaults to FALSE. */ - public static final String KEY_FORCED = "forced"; + public static final String KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle"; /* package private */ MediaFormat(Map<String, Object> map) { mMap = map; diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index 7acf8afe87a1..deba2ccff4cb 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -1606,9 +1606,9 @@ public class MediaPlayer implements SubtitleController.Listener } else if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { mFormat = MediaFormat.createSubtitleFormat( MEDIA_MIMETYPE_TEXT_VTT, language); - mFormat.setInteger(MediaFormat.KEY_AUTOSELECT, in.readInt()); - mFormat.setInteger(MediaFormat.KEY_DEFAULT, in.readInt()); - mFormat.setInteger(MediaFormat.KEY_FORCED, in.readInt()); + mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt()); + mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt()); + mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt()); } else { mFormat = new MediaFormat(); mFormat.setString(MediaFormat.KEY_LANGUAGE, language); @@ -1638,9 +1638,9 @@ public class MediaPlayer implements SubtitleController.Listener dest.writeString(getLanguage()); if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { - dest.writeInt(mFormat.getInteger(MediaFormat.KEY_AUTOSELECT)); - dest.writeInt(mFormat.getInteger(MediaFormat.KEY_DEFAULT)); - dest.writeInt(mFormat.getInteger(MediaFormat.KEY_FORCED)); + dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT)); + dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT)); + dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE)); } } @@ -1765,15 +1765,21 @@ public class MediaPlayer implements SubtitleController.Listener @Override public void onSubtitleTrackSelected(SubtitleTrack track) { if (mSelectedSubtitleTrackIndex >= 0) { - deselectTrack(mSelectedSubtitleTrackIndex); + try { + selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false); + } catch (IllegalStateException e) { + } + mSelectedSubtitleTrackIndex = -1; } - mSelectedSubtitleTrackIndex = -1; setOnSubtitleDataListener(null); for (int i = 0; i < mInbandSubtitleTracks.length; i++) { if (mInbandSubtitleTracks[i] == track) { Log.v(TAG, "Selecting subtitle track " + i); - selectTrack(i); mSelectedSubtitleTrackIndex = i; + try { + selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true); + } catch (IllegalStateException e) { + } setOnSubtitleDataListener(mSubtitleDataListener); break; } @@ -2046,13 +2052,30 @@ public class MediaPlayer implements SubtitleController.Listener private void selectOrDeselectTrack(int index, boolean select) throws IllegalStateException { - // ignore out-of-band tracks - TrackInfo[] trackInfo = getInbandTrackInfo(); - if (index >= trackInfo.length && - index < trackInfo.length + mOutOfBandSubtitleTracks.size()) { + // handle subtitle track through subtitle controller + SubtitleTrack track = null; + if (index < mInbandSubtitleTracks.length) { + track = mInbandSubtitleTracks[index]; + } else if (index < mInbandSubtitleTracks.length + mOutOfBandSubtitleTracks.size()) { + track = mOutOfBandSubtitleTracks.get(index - mInbandSubtitleTracks.length); + } + + if (mSubtitleController != null && track != null) { + if (select) { + mSubtitleController.selectTrack(track); + } else if (mSubtitleController.getSelectedTrack() == track) { + mSubtitleController.selectTrack(null); + } else { + Log.w(TAG, "trying to deselect track that was not selected"); + } return; } + selectOrDeselectInbandTrack(index, select); + } + + private void selectOrDeselectInbandTrack(int index, boolean select) + throws IllegalStateException { Parcel request = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { diff --git a/media/java/android/media/SubtitleController.java b/media/java/android/media/SubtitleController.java index e83c5bae99f3..80905615af21 100644 --- a/media/java/android/media/SubtitleController.java +++ b/media/java/android/media/SubtitleController.java @@ -38,6 +38,21 @@ public class SubtitleController { private boolean mShowing; private CaptioningManager mCaptioningManager; + private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener = + new CaptioningManager.CaptioningChangeListener() { + /** @hide */ + @Override + public void onEnabledChanged(boolean enabled) { + selectDefaultTrack(); + } + + /** @hide */ + @Override + public void onLocaleChanged(Locale locale) { + selectDefaultTrack(); + } + }; + /** * Creates a subtitle controller for a media playback object that implements * the MediaTimeProvider interface. @@ -58,15 +73,24 @@ public class SubtitleController { (CaptioningManager)context.getSystemService(Context.CAPTIONING_SERVICE); } + @Override + protected void finalize() throws Throwable { + mCaptioningManager.removeCaptioningChangeListener( + mCaptioningChangeListener); + super.finalize(); + } + /** * @return the available subtitle tracks for this media. These include * the tracks found by {@link MediaPlayer} as well as any tracks added * manually via {@link #addTrack}. */ public SubtitleTrack[] getTracks() { - SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()]; - mTracks.toArray(tracks); - return tracks; + synchronized(mTracks) { + SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()]; + mTracks.toArray(tracks); + return tracks; + } } /** @@ -88,6 +112,8 @@ public class SubtitleController { * in-band data from the {@link MediaPlayer}. However, this does * not change the subtitle visibility. * + * Must be called from the UI thread. + * * @param track The subtitle track to select. This must be one of the * tracks in {@link #getTracks}. * @return true if the track was successfully selected. @@ -107,7 +133,9 @@ public class SubtitleController { } mSelectedTrack = track; - mAnchor.setSubtitleWidget(getRenderingWidget()); + if (mAnchor != null) { + mAnchor.setSubtitleWidget(getRenderingWidget()); + } if (mSelectedTrack != null) { mSelectedTrack.setTimeProvider(mTimeProvider); @@ -123,56 +151,123 @@ public class SubtitleController { /** * @return the default subtitle track based on system preferences, or null, * if no such track exists in this manager. + * + * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT. + * + * 1. If captioning is disabled, only consider FORCED tracks. Otherwise, + * consider all tracks, but prefer non-FORCED ones. + * 2. If user selected "Default" caption language: + * a. If there is a considered track with DEFAULT=yes, returns that track + * (favor the first one in the current language if there are more than + * one default tracks, or the first in general if none of them are in + * the current language). + * b. Otherwise, if there is a track with AUTOSELECT=yes in the current + * language, return that one. + * c. If there are no default tracks, and no autoselectable tracks in the + * current language, return null. + * 3. If there is a track with the caption language, select that one. Prefer + * the one with AUTOSELECT=no. + * + * The default values for these flags are DEFAULT=no, AUTOSELECT=yes + * and FORCED=no. + * + * Must be called from the UI thread. */ public SubtitleTrack getDefaultTrack() { - Locale locale = mCaptioningManager.getLocale(); - - for (SubtitleTrack track: mTracks) { - MediaFormat format = track.getFormat(); - String language = format.getString(MediaFormat.KEY_LANGUAGE); - // TODO: select track with best renderer. For now, we select first - // track with local's language or first track if locale has none - if (locale == null || - locale.getLanguage().equals("") || - locale.getISO3Language().equals(language) || - locale.getLanguage().equals(language)) { - return track; + SubtitleTrack bestTrack = null; + int bestScore = -1; + + Locale selectedLocale = mCaptioningManager.getLocale(); + Locale locale = selectedLocale; + if (locale == null) { + locale = Locale.getDefault(); + } + boolean selectForced = !mCaptioningManager.isEnabled(); + + synchronized(mTracks) { + for (SubtitleTrack track: mTracks) { + MediaFormat format = track.getFormat(); + String language = format.getString(MediaFormat.KEY_LANGUAGE); + boolean forced = + format.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0; + boolean autoselect = + format.getInteger(MediaFormat.KEY_IS_AUTOSELECT, 1) != 0; + boolean is_default = + format.getInteger(MediaFormat.KEY_IS_DEFAULT, 0) != 0; + + boolean languageMatches = + (locale == null || + locale.getLanguage().equals("") || + locale.getISO3Language().equals(language) || + locale.getLanguage().equals(language)); + // is_default is meaningless unless caption language is 'default' + int score = (forced ? 0 : 8) + + (((selectedLocale == null) && is_default) ? 4 : 0) + + (autoselect ? 0 : 2) + (languageMatches ? 1 : 0); + + if (selectForced && !forced) { + continue; + } + + // we treat null locale/language as matching any language + if ((selectedLocale == null && is_default) || + (languageMatches && + (autoselect || forced || selectedLocale != null))) { + if (score > bestScore) { + bestScore = score; + bestTrack = track; + } + } } } - return null; + return bestTrack; } private boolean mTrackIsExplicit = false; private boolean mVisibilityIsExplicit = false; - /** @hide */ + /** @hide - called from UI thread */ public void selectDefaultTrack() { if (mTrackIsExplicit) { + // If track selection is explicit, but visibility + // is not, it falls back to the captioning setting + if (!mVisibilityIsExplicit) { + if (mCaptioningManager.isEnabled() || + (mSelectedTrack != null && + mSelectedTrack.getFormat().getInteger( + MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) { + show(); + } else { + hide(); + } + mVisibilityIsExplicit = false; + } return; } + // We can have a default (forced) track even if captioning + // is not enabled. This is handled by getDefaultTrack(). + // Show this track unless subtitles were explicitly hidden. SubtitleTrack track = getDefaultTrack(); if (track != null) { selectTrack(track); mTrackIsExplicit = false; if (!mVisibilityIsExplicit) { - if (mCaptioningManager.isEnabled()) { - show(); - } else { - hide(); - } + show(); mVisibilityIsExplicit = false; } } } - /** @hide */ + /** @hide - called from UI thread */ public void reset() { hide(); selectTrack(null); mTracks.clear(); mTrackIsExplicit = false; mVisibilityIsExplicit = false; + mCaptioningManager.removeCaptioningChangeListener( + mCaptioningChangeListener); } /** @@ -183,12 +278,20 @@ public class SubtitleController { * @return the created {@link SubtitleTrack} object */ public SubtitleTrack addTrack(MediaFormat format) { - for (Renderer renderer: mRenderers) { - if (renderer.supports(format)) { - SubtitleTrack track = renderer.createTrack(format); - if (track != null) { - mTracks.add(track); - return track; + synchronized(mRenderers) { + for (Renderer renderer: mRenderers) { + if (renderer.supports(format)) { + SubtitleTrack track = renderer.createTrack(format); + if (track != null) { + synchronized(mTracks) { + if (mTracks.size() == 0) { + mCaptioningManager.addCaptioningChangeListener( + mCaptioningChangeListener); + } + mTracks.add(track); + } + return track; + } } } } @@ -197,6 +300,8 @@ public class SubtitleController { /** * Show the selected (or default) subtitle track. + * + * Must be called from the UI thread. */ public void show() { mShowing = true; @@ -208,6 +313,8 @@ public class SubtitleController { /** * Hide the selected (or default) subtitle track. + * + * Must be called from the UI thread. */ public void hide() { mVisibilityIsExplicit = true; @@ -257,10 +364,12 @@ public class SubtitleController { * support for a subtitle format. */ public void registerRenderer(Renderer renderer) { - // TODO how to get available renderers in the system - if (!mRenderers.contains(renderer)) { - // TODO should added renderers override existing ones (to allow replacing?) - mRenderers.add(renderer); + synchronized(mRenderers) { + // TODO how to get available renderers in the system + if (!mRenderers.contains(renderer)) { + // TODO should added renderers override existing ones (to allow replacing?) + mRenderers.add(renderer); + } } } @@ -279,7 +388,7 @@ public class SubtitleController { private Anchor mAnchor; - /** @hide */ + /** @hide - called from UI thread */ public void setAnchor(Anchor anchor) { if (mAnchor == anchor) { return; diff --git a/media/java/android/media/SubtitleTrack.java b/media/java/android/media/SubtitleTrack.java index cb689af7a43e..06063de66a0b 100644 --- a/media/java/android/media/SubtitleTrack.java +++ b/media/java/android/media/SubtitleTrack.java @@ -69,7 +69,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList } /** @hide */ - public MediaFormat getFormat() { + public final MediaFormat getFormat() { return mFormat; } @@ -201,7 +201,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList } /** @hide */ - public void scheduleTimedEvents() { + protected void scheduleTimedEvents() { /* get times for the next event */ if (mTimeProvider != null) { mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs); @@ -363,7 +363,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList } /** @hide */ - public void setTimeProvider(MediaTimeProvider timeProvider) { + public synchronized void setTimeProvider(MediaTimeProvider timeProvider) { if (mTimeProvider == timeProvider) { return; } |