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