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 android.graphics.Canvas;
20 import android.media.MediaPlayer.TrackInfo;
21 import android.os.Handler;
22 import android.util.Log;
23 import android.util.LongSparseArray;
24 import android.util.Pair;
25 
26 import java.util.Iterator;
27 import java.util.NoSuchElementException;
28 import java.util.SortedMap;
29 import java.util.TreeMap;
30 import 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  */
38 public 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 
SubtitleTrack(MediaFormat format)65     public SubtitleTrack(MediaFormat format) {
66         mFormat = format;
67         mCues = new CueList();
68         clearActiveCues();
69         mLastTimeMs = -1;
70     }
71 
72     /** @hide */
getFormat()73     public final MediaFormat getFormat() {
74         return mFormat;
75     }
76 
77     private long mNextScheduledTimeMs = -1;
78 
onData(SubtitleData data)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      */
onData(byte[] data, boolean eos, long runID)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      */
getRenderingWidget()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      */
updateView(Vector<Cue> activeCues)125     public abstract void updateView(Vector<Cue> activeCues);
126 
127     /** @hide */
updateActiveCues(boolean rebuild, long timeMs)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 
removeRunsByEndTimeIndex(int ix)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
finalize()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 
takeTime(long timeMs)199     private synchronized void takeTime(long timeMs) {
200         mLastTimeMs = timeMs;
201     }
202 
203     /** @hide */
clearActiveCues()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 */
scheduleTimedEvents()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
onTimedEvent(long timeUs)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
onSeek(long timeUs)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
onStop()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 */
show()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 */
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 */
addCue(Cue cue)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 */
setTimeProvider(MediaTimeProvider timeProvider)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 
addEvent(Cue cue, long timeMs)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 
removeEvent(Cue cue, long timeMs)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 
add(Cue cue)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 
remove(Cue cue)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 
entriesBetween( final long lastTimeMs, final long timeMs)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 
nextTimeAfter(long timeMs)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
hasNext()490             public boolean hasNext() {
491                 return !mDone;
492             }
493 
494             @Override
next()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
remove()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 
EntryIterator(SortedMap<Long, Vector<Cue> > cues)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 
nextKey()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 
CueList()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