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