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