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