OSDN Git Service

Add CaptioningManager listener Subtitle support.
authorLajos Molnar <lajos@google.com>
Wed, 11 Sep 2013 23:26:18 +0000 (16:26 -0700)
committerLajos Molnar <lajos@google.com>
Tue, 17 Sep 2013 17:09:18 +0000 (10:09 -0700)
Update default track selection based on settings change.
Implement HLS track selection scheme.
This requires synchronization for subtitletrack and controller.

Change-Id: I8295956d264ab2016a83957ace37d9d7b1acde4c
Signed-off-by: Lajos Molnar <lajos@google.com>
Bug: 10326117

api/current.txt
media/java/android/media/MediaFormat.java
media/java/android/media/MediaPlayer.java
media/java/android/media/SubtitleController.java
media/java/android/media/SubtitleTrack.java

index c7bb3b9..3b0dce1 100644 (file)
@@ -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";
index 16ae43d..1ae8901 100644 (file)
@@ -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;
index 7acf8af..deba2cc 100644 (file)
@@ -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 {
index e83c5ba..8090561 100644 (file)
@@ -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;
index cb689af..06063de 100644 (file)
@@ -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;
         }