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