Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import java.util.Locale;
     20 import java.util.Vector;
     21 
     22 import android.content.Context;
     23 import android.media.MediaPlayer.TrackInfo;
     24 import android.media.SubtitleTrack.RenderingWidget;
     25 import android.os.Handler;
     26 import android.os.Looper;
     27 import android.os.Message;
     28 import android.view.accessibility.CaptioningManager;
     29 
     30 /**
     31  * The subtitle controller provides the architecture to display subtitles for a
     32  * media source.  It allows specifying which tracks to display, on which anchor
     33  * to display them, and also allows adding external, out-of-band subtitle tracks.
     34  *
     35  * @hide
     36  */
     37 public class SubtitleController {
     38     private MediaTimeProvider mTimeProvider;
     39     private Vector<Renderer> mRenderers;
     40     private Vector<SubtitleTrack> mTracks;
     41     private SubtitleTrack mSelectedTrack;
     42     private boolean mShowing;
     43     private CaptioningManager mCaptioningManager;
     44     private Handler mHandler;
     45 
     46     private static final int WHAT_SHOW = 1;
     47     private static final int WHAT_HIDE = 2;
     48     private static final int WHAT_SELECT_TRACK = 3;
     49     private static final int WHAT_SELECT_DEFAULT_TRACK = 4;
     50 
     51     private final Handler.Callback mCallback = new Handler.Callback() {
     52         @Override
     53         public boolean handleMessage(Message msg) {
     54             switch (msg.what) {
     55             case WHAT_SHOW:
     56                 doShow();
     57                 return true;
     58             case WHAT_HIDE:
     59                 doHide();
     60                 return true;
     61             case WHAT_SELECT_TRACK:
     62                 doSelectTrack((SubtitleTrack)msg.obj);
     63                 return true;
     64             case WHAT_SELECT_DEFAULT_TRACK:
     65                 doSelectDefaultTrack();
     66                 return true;
     67             default:
     68                 return false;
     69             }
     70         }
     71     };
     72 
     73     private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
     74         new CaptioningManager.CaptioningChangeListener() {
     75             /** @hide */
     76             @Override
     77             public void onEnabledChanged(boolean enabled) {
     78                 selectDefaultTrack();
     79             }
     80 
     81             /** @hide */
     82             @Override
     83             public void onLocaleChanged(Locale locale) {
     84                 selectDefaultTrack();
     85             }
     86         };
     87 
     88     /**
     89      * Creates a subtitle controller for a media playback object that implements
     90      * the MediaTimeProvider interface.
     91      *
     92      * @param timeProvider
     93      */
     94     public SubtitleController(
     95             Context context,
     96             MediaTimeProvider timeProvider,
     97             Listener listener) {
     98         mTimeProvider = timeProvider;
     99         mListener = listener;
    100 
    101         mRenderers = new Vector<Renderer>();
    102         mShowing = false;
    103         mTracks = new Vector<SubtitleTrack>();
    104         mCaptioningManager =
    105             (CaptioningManager)context.getSystemService(Context.CAPTIONING_SERVICE);
    106     }
    107 
    108     @Override
    109     protected void finalize() throws Throwable {
    110         mCaptioningManager.removeCaptioningChangeListener(
    111                 mCaptioningChangeListener);
    112         super.finalize();
    113     }
    114 
    115     /**
    116      * @return the available subtitle tracks for this media. These include
    117      * the tracks found by {@link MediaPlayer} as well as any tracks added
    118      * manually via {@link #addTrack}.
    119      */
    120     public SubtitleTrack[] getTracks() {
    121         synchronized(mTracks) {
    122             SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
    123             mTracks.toArray(tracks);
    124             return tracks;
    125         }
    126     }
    127 
    128     /**
    129      * @return the currently selected subtitle track
    130      */
    131     public SubtitleTrack getSelectedTrack() {
    132         return mSelectedTrack;
    133     }
    134 
    135     private RenderingWidget getRenderingWidget() {
    136         if (mSelectedTrack == null) {
    137             return null;
    138         }
    139         return mSelectedTrack.getRenderingWidget();
    140     }
    141 
    142     /**
    143      * Selects a subtitle track.  As a result, this track will receive
    144      * in-band data from the {@link MediaPlayer}.  However, this does
    145      * not change the subtitle visibility.
    146      *
    147      * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
    148      *
    149      * @param track The subtitle track to select.  This must be one of the
    150      *              tracks in {@link #getTracks}.
    151      * @return true if the track was successfully selected.
    152      */
    153     public boolean selectTrack(SubtitleTrack track) {
    154         if (track != null && !mTracks.contains(track)) {
    155             return false;
    156         }
    157 
    158         processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_TRACK, track));
    159         return true;
    160     }
    161 
    162     private void doSelectTrack(SubtitleTrack track) {
    163         mTrackIsExplicit = true;
    164         if (mSelectedTrack == track) {
    165             return;
    166         }
    167 
    168         if (mSelectedTrack != null) {
    169             mSelectedTrack.hide();
    170             mSelectedTrack.setTimeProvider(null);
    171         }
    172 
    173         mSelectedTrack = track;
    174         if (mAnchor != null) {
    175             mAnchor.setSubtitleWidget(getRenderingWidget());
    176         }
    177 
    178         if (mSelectedTrack != null) {
    179             mSelectedTrack.setTimeProvider(mTimeProvider);
    180             mSelectedTrack.show();
    181         }
    182 
    183         if (mListener != null) {
    184             mListener.onSubtitleTrackSelected(track);
    185         }
    186     }
    187 
    188     /**
    189      * @return the default subtitle track based on system preferences, or null,
    190      * if no such track exists in this manager.
    191      *
    192      * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
    193      *
    194      * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
    195      * consider all tracks, but prefer non-FORCED ones.
    196      * 2. If user selected "Default" caption language:
    197      *   a. If there is a considered track with DEFAULT=yes, returns that track
    198      *      (favor the first one in the current language if there are more than
    199      *      one default tracks, or the first in general if none of them are in
    200      *      the current language).
    201      *   b. Otherwise, if there is a track with AUTOSELECT=yes in the current
    202      *      language, return that one.
    203      *   c. If there are no default tracks, and no autoselectable tracks in the
    204      *      current language, return null.
    205      * 3. If there is a track with the caption language, select that one.  Prefer
    206      * the one with AUTOSELECT=no.
    207      *
    208      * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
    209      * and FORCED=no.
    210      */
    211     public SubtitleTrack getDefaultTrack() {
    212         SubtitleTrack bestTrack = null;
    213         int bestScore = -1;
    214 
    215         Locale selectedLocale = mCaptioningManager.getLocale();
    216         Locale locale = selectedLocale;
    217         if (locale == null) {
    218             locale = Locale.getDefault();
    219         }
    220         boolean selectForced = !mCaptioningManager.isEnabled();
    221 
    222         synchronized(mTracks) {
    223             for (SubtitleTrack track: mTracks) {
    224                 MediaFormat format = track.getFormat();
    225                 String language = format.getString(MediaFormat.KEY_LANGUAGE);
    226                 boolean forced =
    227                     format.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
    228                 boolean autoselect =
    229                     format.getInteger(MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
    230                 boolean is_default =
    231                     format.getInteger(MediaFormat.KEY_IS_DEFAULT, 0) != 0;
    232 
    233                 boolean languageMatches =
    234                     (locale == null ||
    235                     locale.getLanguage().equals("") ||
    236                     locale.getISO3Language().equals(language) ||
    237                     locale.getLanguage().equals(language));
    238                 // is_default is meaningless unless caption language is 'default'
    239                 int score = (forced ? 0 : 8) +
    240                     (((selectedLocale == null) && is_default) ? 4 : 0) +
    241                     (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
    242 
    243                 if (selectForced && !forced) {
    244                     continue;
    245                 }
    246 
    247                 // we treat null locale/language as matching any language
    248                 if ((selectedLocale == null && is_default) ||
    249                     (languageMatches &&
    250                      (autoselect || forced || selectedLocale != null))) {
    251                     if (score > bestScore) {
    252                         bestScore = score;
    253                         bestTrack = track;
    254                     }
    255                 }
    256             }
    257         }
    258         return bestTrack;
    259     }
    260 
    261     private boolean mTrackIsExplicit = false;
    262     private boolean mVisibilityIsExplicit = false;
    263 
    264     /** @hide - should be called from anchor thread */
    265     public void selectDefaultTrack() {
    266         processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
    267     }
    268 
    269     private void doSelectDefaultTrack() {
    270         if (mTrackIsExplicit) {
    271             // If track selection is explicit, but visibility
    272             // is not, it falls back to the captioning setting
    273             if (!mVisibilityIsExplicit) {
    274                 if (mCaptioningManager.isEnabled() ||
    275                     (mSelectedTrack != null &&
    276                      mSelectedTrack.getFormat().getInteger(
    277                             MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
    278                     show();
    279                 } else if (mSelectedTrack != null
    280                         && mSelectedTrack.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
    281                     hide();
    282                 }
    283                 mVisibilityIsExplicit = false;
    284             }
    285             return;
    286         }
    287 
    288         // We can have a default (forced) track even if captioning
    289         // is not enabled.  This is handled by getDefaultTrack().
    290         // Show this track unless subtitles were explicitly hidden.
    291         SubtitleTrack track = getDefaultTrack();
    292         if (track != null) {
    293             selectTrack(track);
    294             mTrackIsExplicit = false;
    295             if (!mVisibilityIsExplicit) {
    296                 show();
    297                 mVisibilityIsExplicit = false;
    298             }
    299         }
    300     }
    301 
    302     /** @hide - must be called from anchor thread */
    303     public void reset() {
    304         checkAnchorLooper();
    305         hide();
    306         selectTrack(null);
    307         mTracks.clear();
    308         mTrackIsExplicit = false;
    309         mVisibilityIsExplicit = false;
    310         mCaptioningManager.removeCaptioningChangeListener(
    311                 mCaptioningChangeListener);
    312     }
    313 
    314     /**
    315      * Adds a new, external subtitle track to the manager.
    316      *
    317      * @param format the format of the track that will include at least
    318      *               the MIME type {@link MediaFormat@KEY_MIME}.
    319      * @return the created {@link SubtitleTrack} object
    320      */
    321     public SubtitleTrack addTrack(MediaFormat format) {
    322         synchronized(mRenderers) {
    323             for (Renderer renderer: mRenderers) {
    324                 if (renderer.supports(format)) {
    325                     SubtitleTrack track = renderer.createTrack(format);
    326                     if (track != null) {
    327                         synchronized(mTracks) {
    328                             if (mTracks.size() == 0) {
    329                                 mCaptioningManager.addCaptioningChangeListener(
    330                                         mCaptioningChangeListener);
    331                             }
    332                             mTracks.add(track);
    333                         }
    334                         return track;
    335                     }
    336                 }
    337             }
    338         }
    339         return null;
    340     }
    341 
    342     /**
    343      * Show the selected (or default) subtitle track.
    344      *
    345      * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
    346      */
    347     public void show() {
    348         processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
    349     }
    350 
    351     private void doShow() {
    352         mShowing = true;
    353         mVisibilityIsExplicit = true;
    354         if (mSelectedTrack != null) {
    355             mSelectedTrack.show();
    356         }
    357     }
    358 
    359     /**
    360      * Hide the selected (or default) subtitle track.
    361      *
    362      * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
    363      */
    364     public void hide() {
    365         processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
    366     }
    367 
    368     private void doHide() {
    369         mVisibilityIsExplicit = true;
    370         if (mSelectedTrack != null) {
    371             mSelectedTrack.hide();
    372         }
    373         mShowing = false;
    374     }
    375 
    376     /**
    377      * Interface for supporting a single or multiple subtitle types in {@link
    378      * MediaPlayer}.
    379      */
    380     public abstract static class Renderer {
    381         /**
    382          * Called by {@link MediaPlayer}'s {@link SubtitleController} when a new
    383          * subtitle track is detected, to see if it should use this object to
    384          * parse and display this subtitle track.
    385          *
    386          * @param format the format of the track that will include at least
    387          *               the MIME type {@link MediaFormat@KEY_MIME}.
    388          *
    389          * @return true if and only if the track format is supported by this
    390          * renderer
    391          */
    392         public abstract boolean supports(MediaFormat format);
    393 
    394         /**
    395          * Called by {@link MediaPlayer}'s {@link SubtitleController} for each
    396          * subtitle track that was detected and is supported by this object to
    397          * create a {@link SubtitleTrack} object.  This object will be created
    398          * for each track that was found.  If the track is selected for display,
    399          * this object will be used to parse and display the track data.
    400          *
    401          * @param format the format of the track that will include at least
    402          *               the MIME type {@link MediaFormat@KEY_MIME}.
    403          * @return a {@link SubtitleTrack} object that will be used to parse
    404          * and render the subtitle track.
    405          */
    406         public abstract SubtitleTrack createTrack(MediaFormat format);
    407     }
    408 
    409     /**
    410      * Add support for a subtitle format in {@link MediaPlayer}.
    411      *
    412      * @param renderer a {@link SubtitleController.Renderer} object that adds
    413      *                 support for a subtitle format.
    414      */
    415     public void registerRenderer(Renderer renderer) {
    416         synchronized(mRenderers) {
    417             // TODO how to get available renderers in the system
    418             if (!mRenderers.contains(renderer)) {
    419                 // TODO should added renderers override existing ones (to allow replacing?)
    420                 mRenderers.add(renderer);
    421             }
    422         }
    423     }
    424 
    425     /** @hide */
    426     public boolean hasRendererFor(MediaFormat format) {
    427         synchronized(mRenderers) {
    428             // TODO how to get available renderers in the system
    429             for (Renderer renderer: mRenderers) {
    430                 if (renderer.supports(format)) {
    431                     return true;
    432                 }
    433             }
    434             return false;
    435         }
    436     }
    437 
    438     /**
    439      * Subtitle anchor, an object that is able to display a subtitle renderer,
    440      * e.g. a VideoView.
    441      */
    442     public interface Anchor {
    443         /**
    444          * Anchor should use the supplied subtitle rendering widget, or
    445          * none if it is null.
    446          * @hide
    447          */
    448         public void setSubtitleWidget(RenderingWidget subtitleWidget);
    449 
    450         /**
    451          * Anchors provide the looper on which all track visibility changes
    452          * (track.show/hide, setSubtitleWidget) will take place.
    453          * @hide
    454          */
    455         public Looper getSubtitleLooper();
    456     }
    457 
    458     private Anchor mAnchor;
    459 
    460     /**
    461      *  @hide - called from anchor's looper (if any, both when unsetting and
    462      *  setting)
    463      */
    464     public void setAnchor(Anchor anchor) {
    465         if (mAnchor == anchor) {
    466             return;
    467         }
    468 
    469         if (mAnchor != null) {
    470             checkAnchorLooper();
    471             mAnchor.setSubtitleWidget(null);
    472         }
    473         mAnchor = anchor;
    474         mHandler = null;
    475         if (mAnchor != null) {
    476             mHandler = new Handler(mAnchor.getSubtitleLooper(), mCallback);
    477             checkAnchorLooper();
    478             mAnchor.setSubtitleWidget(getRenderingWidget());
    479         }
    480     }
    481 
    482     private void checkAnchorLooper() {
    483         assert mHandler != null : "Should have a looper already";
    484         assert Looper.myLooper() == mHandler.getLooper() : "Must be called from the anchor's looper";
    485     }
    486 
    487     private void processOnAnchor(Message m) {
    488         assert mHandler != null : "Should have a looper already";
    489         if (Looper.myLooper() == mHandler.getLooper()) {
    490             mHandler.dispatchMessage(m);
    491         } else {
    492             mHandler.sendMessage(m);
    493         }
    494     }
    495 
    496     public interface Listener {
    497         /**
    498          * Called when a subtitle track has been selected.
    499          *
    500          * @param track selected subtitle track or null
    501          * @hide
    502          */
    503         public void onSubtitleTrackSelected(SubtitleTrack track);
    504     }
    505 
    506     private Listener mListener;
    507 }
    508