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