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