Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d486f965 authored by Lajos Molnar's avatar Lajos Molnar
Browse files

Add CaptioningManager listener Subtitle support.



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: default avatarLajos Molnar <lajos@google.com>
Bug: 10326117
parent af672998
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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";
+23 −13
Original line number Diff line number Diff line
@@ -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;
+36 −13
Original line number Diff line number Diff line
@@ -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;
        }
        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 {
+144 −35
Original line number Diff line number Diff line
@@ -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,16 +73,25 @@ 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() {
        synchronized(mTracks) {
            SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
            mTracks.toArray(tracks);
            return tracks;
        }
    }

    /**
     * @return the currently selected subtitle track
@@ -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;
        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();
        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);
            // 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 ||
                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)) {
                return track;
                    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();
                }
                mVisibilityIsExplicit = false;
            }
        }
    }

    /** @hide */
    /** @hide - called from UI thread */
    public void reset() {
        hide();
        selectTrack(null);
        mTracks.clear();
        mTrackIsExplicit = false;
        mVisibilityIsExplicit = false;
        mCaptioningManager.removeCaptioningChangeListener(
                mCaptioningChangeListener);
    }

    /**
@@ -183,20 +278,30 @@ public class SubtitleController {
     * @return the created {@link SubtitleTrack} object
     */
    public SubtitleTrack addTrack(MediaFormat format) {
        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;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 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,12 +364,14 @@ public class SubtitleController {
     *                 support for a subtitle format.
     */
    public void registerRenderer(Renderer 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);
            }
        }
    }

    /**
     * Subtitle anchor, an object that is able to display a subtitle 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;
+3 −3
Original line number Diff line number Diff line
@@ -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;
        }