Home | History | Annotate | Download | only in subtitle
      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