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 17package android.media; 18 19import android.graphics.Canvas; 20import android.media.MediaPlayer.TrackInfo; 21import android.os.Handler; 22import android.util.Log; 23import android.util.LongSparseArray; 24import android.util.Pair; 25 26import java.util.Iterator; 27import java.util.NoSuchElementException; 28import java.util.SortedMap; 29import java.util.TreeMap; 30import java.util.Vector; 31 32/** 33 * A subtitle track abstract base class that is responsible for parsing and displaying 34 * an instance of a particular type of subtitle. 35 * 36 * @hide 37 */ 38public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeListener { 39 private static final String TAG = "SubtitleTrack"; 40 private long mLastUpdateTimeMs; 41 private long mLastTimeMs; 42 43 private Runnable mRunnable; 44 45 /** @hide TODO private */ 46 final protected LongSparseArray<Run> mRunsByEndTime = new LongSparseArray<Run>(); 47 /** @hide TODO private */ 48 final protected LongSparseArray<Run> mRunsByID = new LongSparseArray<Run>(); 49 50 /** @hide TODO private */ 51 protected CueList mCues; 52 /** @hide TODO private */ 53 final protected Vector<Cue> mActiveCues = new Vector<Cue>(); 54 /** @hide */ 55 protected boolean mVisible; 56 57 /** @hide */ 58 public boolean DEBUG = false; 59 60 /** @hide */ 61 protected Handler mHandler = new Handler(); 62 63 private MediaFormat mFormat; 64 65 public SubtitleTrack(MediaFormat format) { 66 mFormat = format; 67 mCues = new CueList(); 68 clearActiveCues(); 69 mLastTimeMs = -1; 70 } 71 72 /** @hide */ 73 public final MediaFormat getFormat() { 74 return mFormat; 75 } 76 77 private long mNextScheduledTimeMs = -1; 78 79 protected void onData(SubtitleData data) { 80 long runID = data.getStartTimeUs() + 1; 81 onData(data.getData(), true /* eos */, runID); 82 setRunDiscardTimeMs( 83 runID, 84 (data.getStartTimeUs() + data.getDurationUs()) / 1000); 85 } 86 87 /** 88 * Called when there is input data for the subtitle track. The 89 * complete subtitle for a track can include multiple whole units 90 * (runs). Each of these units can have multiple sections. The 91 * contents of a run are submitted in sequential order, with eos 92 * indicating the last section of the run. Calls from different 93 * runs must not be intermixed. 94 * 95 * @param data subtitle data byte buffer 96 * @param eos true if this is the last section of the run. 97 * @param runID mostly-unique ID for this run of data. Subtitle cues 98 * with runID of 0 are discarded immediately after 99 * display. Cues with runID of ~0 are discarded 100 * only at the deletion of the track object. Cues 101 * with other runID-s are discarded at the end of the 102 * run, which defaults to the latest timestamp of 103 * any of its cues (with this runID). 104 */ 105 public abstract void onData(byte[] data, boolean eos, long runID); 106 107 /** 108 * Called when adding the subtitle rendering widget to the view hierarchy, 109 * as well as when showing or hiding the subtitle track, or when the video 110 * surface position has changed. 111 * 112 * @return the widget that renders this subtitle track. For most renderers 113 * there should be a single shared instance that is used for all 114 * tracks supported by that renderer, as at most one subtitle track 115 * is visible at one time. 116 */ 117 public abstract RenderingWidget getRenderingWidget(); 118 119 /** 120 * Called when the active cues have changed, and the contents of the subtitle 121 * view should be updated. 122 * 123 * @hide 124 */ 125 public abstract void updateView(Vector<Cue> activeCues); 126 127 /** @hide */ 128 protected synchronized void updateActiveCues(boolean rebuild, long timeMs) { 129 // out-of-order times mean seeking or new active cues being added 130 // (during their own timespan) 131 if (rebuild || mLastUpdateTimeMs > timeMs) { 132 clearActiveCues(); 133 } 134 135 for(Iterator<Pair<Long, Cue> > it = 136 mCues.entriesBetween(mLastUpdateTimeMs, timeMs).iterator(); it.hasNext(); ) { 137 Pair<Long, Cue> event = it.next(); 138 Cue cue = event.second; 139 140 if (cue.mEndTimeMs == event.first) { 141 // remove past cues 142 if (DEBUG) Log.v(TAG, "Removing " + cue); 143 mActiveCues.remove(cue); 144 if (cue.mRunID == 0) { 145 it.remove(); 146 } 147 } else if (cue.mStartTimeMs == event.first) { 148 // add new cues 149 // TRICKY: this will happen in start order 150 if (DEBUG) Log.v(TAG, "Adding " + cue); 151 if (cue.mInnerTimesMs != null) { 152 cue.onTime(timeMs); 153 } 154 mActiveCues.add(cue); 155 } else if (cue.mInnerTimesMs != null) { 156 // cue is modified 157 cue.onTime(timeMs); 158 } 159 } 160 161 /* complete any runs */ 162 while (mRunsByEndTime.size() > 0 && 163 mRunsByEndTime.keyAt(0) <= timeMs) { 164 removeRunsByEndTimeIndex(0); // removes element 165 } 166 mLastUpdateTimeMs = timeMs; 167 } 168 169 private void removeRunsByEndTimeIndex(int ix) { 170 Run run = mRunsByEndTime.valueAt(ix); 171 while (run != null) { 172 Cue cue = run.mFirstCue; 173 while (cue != null) { 174 mCues.remove(cue); 175 Cue nextCue = cue.mNextInRun; 176 cue.mNextInRun = null; 177 cue = nextCue; 178 } 179 mRunsByID.remove(run.mRunID); 180 Run nextRun = run.mNextRunAtEndTimeMs; 181 run.mPrevRunAtEndTimeMs = null; 182 run.mNextRunAtEndTimeMs = null; 183 run = nextRun; 184 } 185 mRunsByEndTime.removeAt(ix); 186 } 187 188 @Override 189 protected void finalize() throws Throwable { 190 /* remove all cues (untangle all cross-links) */ 191 int size = mRunsByEndTime.size(); 192 for(int ix = size - 1; ix >= 0; ix--) { 193 removeRunsByEndTimeIndex(ix); 194 } 195 196 super.finalize(); 197 } 198 199 private synchronized void takeTime(long timeMs) { 200 mLastTimeMs = timeMs; 201 } 202 203 /** @hide */ 204 protected synchronized void clearActiveCues() { 205 if (DEBUG) Log.v(TAG, "Clearing " + mActiveCues.size() + " active cues"); 206 mActiveCues.clear(); 207 mLastUpdateTimeMs = -1; 208 } 209 210 /** @hide */ 211 protected void scheduleTimedEvents() { 212 /* get times for the next event */ 213 if (mTimeProvider != null) { 214 mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs); 215 if (DEBUG) Log.d(TAG, "sched @" + mNextScheduledTimeMs + " after " + mLastTimeMs); 216 mTimeProvider.notifyAt( 217 mNextScheduledTimeMs >= 0 ? 218 (mNextScheduledTimeMs * 1000) : MediaTimeProvider.NO_TIME, 219 this); 220 } 221 } 222 223 /** 224 * @hide 225 */ 226 @Override 227 public void onTimedEvent(long timeUs) { 228 if (DEBUG) Log.d(TAG, "onTimedEvent " + timeUs); 229 synchronized (this) { 230 long timeMs = timeUs / 1000; 231 updateActiveCues(false, timeMs); 232 takeTime(timeMs); 233 } 234 updateView(mActiveCues); 235 scheduleTimedEvents(); 236 } 237 238 /** 239 * @hide 240 */ 241 @Override 242 public void onSeek(long timeUs) { 243 if (DEBUG) Log.d(TAG, "onSeek " + timeUs); 244 synchronized (this) { 245 long timeMs = timeUs / 1000; 246 updateActiveCues(true, timeMs); 247 takeTime(timeMs); 248 } 249 updateView(mActiveCues); 250 scheduleTimedEvents(); 251 } 252 253 /** 254 * @hide 255 */ 256 @Override 257 public void onStop() { 258 synchronized (this) { 259 if (DEBUG) Log.d(TAG, "onStop"); 260 clearActiveCues(); 261 mLastTimeMs = -1; 262 } 263 updateView(mActiveCues); 264 mNextScheduledTimeMs = -1; 265 mTimeProvider.notifyAt(MediaTimeProvider.NO_TIME, this); 266 } 267 268 /** @hide */ 269 protected MediaTimeProvider mTimeProvider; 270 271 /** @hide */ 272 public void show() { 273 if (mVisible) { 274 return; 275 } 276 277 mVisible = true; 278 RenderingWidget renderingWidget = getRenderingWidget(); 279 if (renderingWidget != null) { 280 renderingWidget.setVisible(true); 281 } 282 if (mTimeProvider != null) { 283 mTimeProvider.scheduleUpdate(this); 284 } 285 } 286 287 /** @hide */ 288 public void hide() { 289 if (!mVisible) { 290 return; 291 } 292 293 if (mTimeProvider != null) { 294 mTimeProvider.cancelNotifications(this); 295 } 296 RenderingWidget renderingWidget = getRenderingWidget(); 297 if (renderingWidget != null) { 298 renderingWidget.setVisible(false); 299 } 300 mVisible = false; 301 } 302 303 /** @hide */ 304 protected synchronized boolean addCue(Cue cue) { 305 mCues.add(cue); 306 307 if (cue.mRunID != 0) { 308 Run run = mRunsByID.get(cue.mRunID); 309 if (run == null) { 310 run = new Run(); 311 mRunsByID.put(cue.mRunID, run); 312 run.mEndTimeMs = cue.mEndTimeMs; 313 } else if (run.mEndTimeMs < cue.mEndTimeMs) { 314 run.mEndTimeMs = cue.mEndTimeMs; 315 } 316 317 // link-up cues in the same run 318 cue.mNextInRun = run.mFirstCue; 319 run.mFirstCue = cue; 320 } 321 322 // if a cue is added that should be visible, need to refresh view 323 long nowMs = -1; 324 if (mTimeProvider != null) { 325 try { 326 nowMs = mTimeProvider.getCurrentTimeUs( 327 false /* precise */, true /* monotonic */) / 1000; 328 } catch (IllegalStateException e) { 329 // handle as it we are not playing 330 } 331 } 332 333 if (DEBUG) Log.v(TAG, "mVisible=" + mVisible + ", " + 334 cue.mStartTimeMs + " <= " + nowMs + ", " + 335 cue.mEndTimeMs + " >= " + mLastTimeMs); 336 337 if (mVisible && 338 cue.mStartTimeMs <= nowMs && 339 // we don't trust nowMs, so check any cue since last callback 340 cue.mEndTimeMs >= mLastTimeMs) { 341 if (mRunnable != null) { 342 mHandler.removeCallbacks(mRunnable); 343 } 344 final SubtitleTrack track = this; 345 final long thenMs = nowMs; 346 mRunnable = new Runnable() { 347 @Override 348 public void run() { 349 // even with synchronized, it is possible that we are going 350 // to do multiple updates as the runnable could be already 351 // running. 352 synchronized (track) { 353 mRunnable = null; 354 updateActiveCues(true, thenMs); 355 updateView(mActiveCues); 356 } 357 } 358 }; 359 // delay update so we don't update view on every cue. TODO why 10? 360 if (mHandler.postDelayed(mRunnable, 10 /* delay */)) { 361 if (DEBUG) Log.v(TAG, "scheduling update"); 362 } else { 363 if (DEBUG) Log.w(TAG, "failed to schedule subtitle view update"); 364 } 365 return true; 366 } 367 368 if (mVisible && 369 cue.mEndTimeMs >= mLastTimeMs && 370 (cue.mStartTimeMs < mNextScheduledTimeMs || 371 mNextScheduledTimeMs < 0)) { 372 scheduleTimedEvents(); 373 } 374 375 return false; 376 } 377 378 /** @hide */ 379 public synchronized void setTimeProvider(MediaTimeProvider timeProvider) { 380 if (mTimeProvider == timeProvider) { 381 return; 382 } 383 if (mTimeProvider != null) { 384 mTimeProvider.cancelNotifications(this); 385 } 386 mTimeProvider = timeProvider; 387 if (mTimeProvider != null) { 388 mTimeProvider.scheduleUpdate(this); 389 } 390 } 391 392 393 /** @hide */ 394 static class CueList { 395 private static final String TAG = "CueList"; 396 // simplistic, inefficient implementation 397 private SortedMap<Long, Vector<Cue> > mCues; 398 public boolean DEBUG = false; 399 400 private boolean addEvent(Cue cue, long timeMs) { 401 Vector<Cue> cues = mCues.get(timeMs); 402 if (cues == null) { 403 cues = new Vector<Cue>(2); 404 mCues.put(timeMs, cues); 405 } else if (cues.contains(cue)) { 406 // do not duplicate cues 407 return false; 408 } 409 410 cues.add(cue); 411 return true; 412 } 413 414 private void removeEvent(Cue cue, long timeMs) { 415 Vector<Cue> cues = mCues.get(timeMs); 416 if (cues != null) { 417 cues.remove(cue); 418 if (cues.size() == 0) { 419 mCues.remove(timeMs); 420 } 421 } 422 } 423 424 public void add(Cue cue) { 425 // ignore non-positive-duration cues 426 if (cue.mStartTimeMs >= cue.mEndTimeMs) 427 return; 428 429 if (!addEvent(cue, cue.mStartTimeMs)) { 430 return; 431 } 432 433 long lastTimeMs = cue.mStartTimeMs; 434 if (cue.mInnerTimesMs != null) { 435 for (long timeMs: cue.mInnerTimesMs) { 436 if (timeMs > lastTimeMs && timeMs < cue.mEndTimeMs) { 437 addEvent(cue, timeMs); 438 lastTimeMs = timeMs; 439 } 440 } 441 } 442 443 addEvent(cue, cue.mEndTimeMs); 444 } 445 446 public void remove(Cue cue) { 447 removeEvent(cue, cue.mStartTimeMs); 448 if (cue.mInnerTimesMs != null) { 449 for (long timeMs: cue.mInnerTimesMs) { 450 removeEvent(cue, timeMs); 451 } 452 } 453 removeEvent(cue, cue.mEndTimeMs); 454 } 455 456 public Iterable<Pair<Long, Cue>> entriesBetween( 457 final long lastTimeMs, final long timeMs) { 458 return new Iterable<Pair<Long, Cue> >() { 459 @Override 460 public Iterator<Pair<Long, Cue> > iterator() { 461 if (DEBUG) Log.d(TAG, "slice (" + lastTimeMs + ", " + timeMs + "]="); 462 try { 463 return new EntryIterator( 464 mCues.subMap(lastTimeMs + 1, timeMs + 1)); 465 } catch(IllegalArgumentException e) { 466 return new EntryIterator(null); 467 } 468 } 469 }; 470 } 471 472 public long nextTimeAfter(long timeMs) { 473 SortedMap<Long, Vector<Cue>> tail = null; 474 try { 475 tail = mCues.tailMap(timeMs + 1); 476 if (tail != null) { 477 return tail.firstKey(); 478 } else { 479 return -1; 480 } 481 } catch(IllegalArgumentException e) { 482 return -1; 483 } catch(NoSuchElementException e) { 484 return -1; 485 } 486 } 487 488 class EntryIterator implements Iterator<Pair<Long, Cue> > { 489 @Override 490 public boolean hasNext() { 491 return !mDone; 492 } 493 494 @Override 495 public Pair<Long, Cue> next() { 496 if (mDone) { 497 throw new NoSuchElementException(""); 498 } 499 mLastEntry = new Pair<Long, Cue>( 500 mCurrentTimeMs, mListIterator.next()); 501 mLastListIterator = mListIterator; 502 if (!mListIterator.hasNext()) { 503 nextKey(); 504 } 505 return mLastEntry; 506 } 507 508 @Override 509 public void remove() { 510 // only allow removing end tags 511 if (mLastListIterator == null || 512 mLastEntry.second.mEndTimeMs != mLastEntry.first) { 513 throw new IllegalStateException(""); 514 } 515 516 // remove end-cue 517 mLastListIterator.remove(); 518 mLastListIterator = null; 519 if (mCues.get(mLastEntry.first).size() == 0) { 520 mCues.remove(mLastEntry.first); 521 } 522 523 // remove rest of the cues 524 Cue cue = mLastEntry.second; 525 removeEvent(cue, cue.mStartTimeMs); 526 if (cue.mInnerTimesMs != null) { 527 for (long timeMs: cue.mInnerTimesMs) { 528 removeEvent(cue, timeMs); 529 } 530 } 531 } 532 533 public EntryIterator(SortedMap<Long, Vector<Cue> > cues) { 534 if (DEBUG) Log.v(TAG, cues + ""); 535 mRemainingCues = cues; 536 mLastListIterator = null; 537 nextKey(); 538 } 539 540 private void nextKey() { 541 do { 542 try { 543 if (mRemainingCues == null) { 544 throw new NoSuchElementException(""); 545 } 546 mCurrentTimeMs = mRemainingCues.firstKey(); 547 mListIterator = 548 mRemainingCues.get(mCurrentTimeMs).iterator(); 549 try { 550 mRemainingCues = 551 mRemainingCues.tailMap(mCurrentTimeMs + 1); 552 } catch (IllegalArgumentException e) { 553 mRemainingCues = null; 554 } 555 mDone = false; 556 } catch (NoSuchElementException e) { 557 mDone = true; 558 mRemainingCues = null; 559 mListIterator = null; 560 return; 561 } 562 } while (!mListIterator.hasNext()); 563 } 564 565 private long mCurrentTimeMs; 566 private Iterator<Cue> mListIterator; 567 private boolean mDone; 568 private SortedMap<Long, Vector<Cue> > mRemainingCues; 569 private Iterator<Cue> mLastListIterator; 570 private Pair<Long,Cue> mLastEntry; 571 } 572 573 CueList() { 574 mCues = new TreeMap<Long, Vector<Cue>>(); 575 } 576 } 577 578 /** @hide */ 579 public static class Cue { 580 public long mStartTimeMs; 581 public long mEndTimeMs; 582 public long[] mInnerTimesMs; 583 public long mRunID; 584 585 /** @hide */ 586 public Cue mNextInRun; 587 588 public void onTime(long timeMs) { } 589 } 590 591 /** @hide update mRunsByEndTime (with default end time) */ 592 protected void finishedRun(long runID) { 593 if (runID != 0 && runID != ~0) { 594 Run run = mRunsByID.get(runID); 595 if (run != null) { 596 run.storeByEndTimeMs(mRunsByEndTime); 597 } 598 } 599 } 600 601 /** @hide update mRunsByEndTime with given end time */ 602 public void setRunDiscardTimeMs(long runID, long timeMs) { 603 if (runID != 0 && runID != ~0) { 604 Run run = mRunsByID.get(runID); 605 if (run != null) { 606 run.mEndTimeMs = timeMs; 607 run.storeByEndTimeMs(mRunsByEndTime); 608 } 609 } 610 } 611 612 /** @hide whether this is a text track who fires events instead getting rendered */ 613 public int getTrackType() { 614 return getRenderingWidget() == null 615 ? TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT 616 : TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE; 617 } 618 619 620 /** @hide */ 621 private static class Run { 622 public Cue mFirstCue; 623 public Run mNextRunAtEndTimeMs; 624 public Run mPrevRunAtEndTimeMs; 625 public long mEndTimeMs = -1; 626 public long mRunID = 0; 627 private long mStoredEndTimeMs = -1; 628 629 public void storeByEndTimeMs(LongSparseArray<Run> runsByEndTime) { 630 // remove old value if any 631 int ix = runsByEndTime.indexOfKey(mStoredEndTimeMs); 632 if (ix >= 0) { 633 if (mPrevRunAtEndTimeMs == null) { 634 assert(this == runsByEndTime.valueAt(ix)); 635 if (mNextRunAtEndTimeMs == null) { 636 runsByEndTime.removeAt(ix); 637 } else { 638 runsByEndTime.setValueAt(ix, mNextRunAtEndTimeMs); 639 } 640 } 641 removeAtEndTimeMs(); 642 } 643 644 // add new value 645 if (mEndTimeMs >= 0) { 646 mPrevRunAtEndTimeMs = null; 647 mNextRunAtEndTimeMs = runsByEndTime.get(mEndTimeMs); 648 if (mNextRunAtEndTimeMs != null) { 649 mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = this; 650 } 651 runsByEndTime.put(mEndTimeMs, this); 652 mStoredEndTimeMs = mEndTimeMs; 653 } 654 } 655 656 public void removeAtEndTimeMs() { 657 Run prev = mPrevRunAtEndTimeMs; 658 659 if (mPrevRunAtEndTimeMs != null) { 660 mPrevRunAtEndTimeMs.mNextRunAtEndTimeMs = mNextRunAtEndTimeMs; 661 mPrevRunAtEndTimeMs = null; 662 } 663 if (mNextRunAtEndTimeMs != null) { 664 mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = prev; 665 mNextRunAtEndTimeMs = null; 666 } 667 } 668 } 669 670 /** 671 * Interface for rendering subtitles onto a Canvas. 672 */ 673 public interface RenderingWidget { 674 /** 675 * Sets the widget's callback, which is used to send updates when the 676 * rendered data has changed. 677 * 678 * @param callback update callback 679 */ 680 public void setOnChangedListener(OnChangedListener callback); 681 682 /** 683 * Sets the widget's size. 684 * 685 * @param width width in pixels 686 * @param height height in pixels 687 */ 688 public void setSize(int width, int height); 689 690 /** 691 * Sets whether the widget should draw subtitles. 692 * 693 * @param visible true if subtitles should be drawn, false otherwise 694 */ 695 public void setVisible(boolean visible); 696 697 /** 698 * Renders subtitles onto a {@link Canvas}. 699 * 700 * @param c canvas on which to render subtitles 701 */ 702 public void draw(Canvas c); 703 704 /** 705 * Called when the widget is attached to a window. 706 */ 707 public void onAttachedToWindow(); 708 709 /** 710 * Called when the widget is detached from a window. 711 */ 712 public void onDetachedFromWindow(); 713 714 /** 715 * Callback used to send updates about changes to rendering data. 716 */ 717 public interface OnChangedListener { 718 /** 719 * Called when the rendering data has changed. 720 * 721 * @param renderingWidget the widget whose data has changed 722 */ 723 public void onChanged(RenderingWidget renderingWidget); 724 } 725 } 726} 727