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