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      */
SubtitleController( Context context, MediaTimeProvider timeProvider, Listener listener)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
finalize()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      */
getTracks()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      */
getSelectedTrack()131     public SubtitleTrack getSelectedTrack() {
132         return mSelectedTrack;
133     }
134 
getRenderingWidget()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      */
selectTrack(SubtitleTrack track)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 
doSelectTrack(SubtitleTrack track)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      */
getDefaultTrack()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 */
selectDefaultTrack()265     public void selectDefaultTrack() {
266         processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
267     }
268 
doSelectDefaultTrack()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 */
reset()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      */
addTrack(MediaFormat format)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      */
show()347     public void show() {
348         processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
349     }
350 
doShow()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      */
hide()364     public void hide() {
365         processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
366     }
367 
doHide()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          */
supports(MediaFormat format)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          */
createTrack(MediaFormat format)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      */
registerRenderer(Renderer renderer)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 */
hasRendererFor(MediaFormat format)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          */
setSubtitleWidget(RenderingWidget subtitleWidget)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          */
getSubtitleLooper()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      */
setAnchor(Anchor anchor)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 
checkAnchorLooper()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 
processOnAnchor(Message m)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          */
onSubtitleTrackSelected(SubtitleTrack track)503         public void onSubtitleTrackSelected(SubtitleTrack track);
504     }
505 
506     private Listener mListener;
507 }
508