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 17package com.android.media.subtitle; 18 19import java.util.Locale; 20import java.util.Vector; 21 22import android.content.Context; 23import android.media.MediaFormat; 24import android.media.MediaPlayer2; 25import android.media.MediaPlayer2.TrackInfo; 26import android.os.Handler; 27import android.os.Looper; 28import android.os.Message; 29import android.view.accessibility.CaptioningManager; 30 31import 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 */ 39public 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 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 */ 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 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 */ 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 */ 135 public SubtitleTrack getSelectedTrack() { 136 return mSelectedTrack; 137 } 138 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 */ 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 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 */ 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 */ 269 public void selectDefaultTrack() { 270 processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK)); 271 } 272 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 */ 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 public void show() { 352 processOnAnchor(mHandler.obtainMessage(WHAT_SHOW)); 353 } 354 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 */ 368 public void hide() { 369 processOnAnchor(mHandler.obtainMessage(WHAT_HIDE)); 370 } 371 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 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 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 */ 504 public void onSubtitleTrackSelected(SubtitleTrack track); 505 } 506 507 private Listener mListener; 508} 509