blob: 48657a6c810fdd15b258ed8a6cc2676e2105da7c [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
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
17package android.media;
18
19import android.compat.annotation.UnsupportedAppUsage;
20import android.content.Context;
21import android.media.MediaPlayer.TrackInfo;
22import android.media.SubtitleTrack.RenderingWidget;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.view.accessibility.CaptioningManager;
27
28import java.util.Locale;
29import 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 */
38public 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
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
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 */
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 */
134 public SubtitleTrack getSelectedTrack() {
135 return mSelectedTrack;
136 }
137
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 */
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
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 */
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 */
268 public void selectDefaultTrack() {
269 processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
270 }
271
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
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 */
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
352 public void show() {
353 processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
354 }
355
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
370 public void hide() {
371 processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
372 }
373
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 */
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 */
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
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 */
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 */
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 */
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 */
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
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
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 */
510 public void onSubtitleTrackSelected(SubtitleTrack track);
511 }
512
513 private Listener mListener;
514}