Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2014 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.content.Context;
     20 import android.text.TextUtils;
     21 import android.util.AttributeSet;
     22 import android.util.Log;
     23 import android.view.Gravity;
     24 import android.view.View;
     25 import android.view.accessibility.CaptioningManager;
     26 import android.widget.LinearLayout;
     27 import android.widget.TextView;
     28 
     29 import java.io.IOException;
     30 import java.io.StringReader;
     31 import java.util.ArrayList;
     32 import java.util.LinkedList;
     33 import java.util.List;
     34 import java.util.TreeSet;
     35 import java.util.Vector;
     36 import java.util.regex.Matcher;
     37 import java.util.regex.Pattern;
     38 
     39 import org.xmlpull.v1.XmlPullParser;
     40 import org.xmlpull.v1.XmlPullParserException;
     41 import org.xmlpull.v1.XmlPullParserFactory;
     42 
     43 /** @hide */
     44 public class TtmlRenderer extends SubtitleController.Renderer {
     45     private final Context mContext;
     46 
     47     private static final String MEDIA_MIMETYPE_TEXT_TTML = "application/ttml+xml";
     48 
     49     private TtmlRenderingWidget mRenderingWidget;
     50 
     51     public TtmlRenderer(Context context) {
     52         mContext = context;
     53     }
     54 
     55     @Override
     56     public boolean supports(MediaFormat format) {
     57         if (format.containsKey(MediaFormat.KEY_MIME)) {
     58             return format.getString(MediaFormat.KEY_MIME).equals(MEDIA_MIMETYPE_TEXT_TTML);
     59         }
     60         return false;
     61     }
     62 
     63     @Override
     64     public SubtitleTrack createTrack(MediaFormat format) {
     65         if (mRenderingWidget == null) {
     66             mRenderingWidget = new TtmlRenderingWidget(mContext);
     67         }
     68         return new TtmlTrack(mRenderingWidget, format);
     69     }
     70 }
     71 
     72 /**
     73  * A class which provides utillity methods for TTML parsing.
     74  *
     75  * @hide
     76  */
     77 final class TtmlUtils {
     78     public static final String TAG_TT = "tt";
     79     public static final String TAG_HEAD = "head";
     80     public static final String TAG_BODY = "body";
     81     public static final String TAG_DIV = "div";
     82     public static final String TAG_P = "p";
     83     public static final String TAG_SPAN = "span";
     84     public static final String TAG_BR = "br";
     85     public static final String TAG_STYLE = "style";
     86     public static final String TAG_STYLING = "styling";
     87     public static final String TAG_LAYOUT = "layout";
     88     public static final String TAG_REGION = "region";
     89     public static final String TAG_METADATA = "metadata";
     90     public static final String TAG_SMPTE_IMAGE = "smpte:image";
     91     public static final String TAG_SMPTE_DATA = "smpte:data";
     92     public static final String TAG_SMPTE_INFORMATION = "smpte:information";
     93     public static final String PCDATA = "#pcdata";
     94     public static final String ATTR_BEGIN = "begin";
     95     public static final String ATTR_DURATION = "dur";
     96     public static final String ATTR_END = "end";
     97     public static final long INVALID_TIMESTAMP = Long.MAX_VALUE;
     98 
     99     /**
    100      * Time expression RE according to the spec:
    101      * http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression
    102      */
    103     private static final Pattern CLOCK_TIME = Pattern.compile(
    104             "^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
    105             + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
    106 
    107     private static final Pattern OFFSET_TIME = Pattern.compile(
    108             "^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
    109 
    110     private TtmlUtils() {
    111     }
    112 
    113     /**
    114      * Parses the given time expression and returns a timestamp in millisecond.
    115      * <p>
    116      * For the format of the time expression, please refer <a href=
    117      * "http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
    118      *
    119      * @param time A string which includes time expression.
    120      * @param frameRate the framerate of the stream.
    121      * @param subframeRate the sub-framerate of the stream
    122      * @param tickRate the tick rate of the stream.
    123      * @return the parsed timestamp in micro-second.
    124      * @throws NumberFormatException if the given string does not match to the
    125      *             format.
    126      */
    127     public static long parseTimeExpression(String time, int frameRate, int subframeRate,
    128             int tickRate) throws NumberFormatException {
    129         Matcher matcher = CLOCK_TIME.matcher(time);
    130         if (matcher.matches()) {
    131             String hours = matcher.group(1);
    132             double durationSeconds = Long.parseLong(hours) * 3600;
    133             String minutes = matcher.group(2);
    134             durationSeconds += Long.parseLong(minutes) * 60;
    135             String seconds = matcher.group(3);
    136             durationSeconds += Long.parseLong(seconds);
    137             String fraction = matcher.group(4);
    138             durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
    139             String frames = matcher.group(5);
    140             durationSeconds += (frames != null) ? ((double)Long.parseLong(frames)) / frameRate : 0;
    141             String subframes = matcher.group(6);
    142             durationSeconds += (subframes != null) ? ((double)Long.parseLong(subframes))
    143                     / subframeRate / frameRate
    144                     : 0;
    145             return (long)(durationSeconds * 1000);
    146         }
    147         matcher = OFFSET_TIME.matcher(time);
    148         if (matcher.matches()) {
    149             String timeValue = matcher.group(1);
    150             double value = Double.parseDouble(timeValue);
    151             String unit = matcher.group(2);
    152             if (unit.equals("h")) {
    153                 value *= 3600L * 1000000L;
    154             } else if (unit.equals("m")) {
    155                 value *= 60 * 1000000;
    156             } else if (unit.equals("s")) {
    157                 value *= 1000000;
    158             } else if (unit.equals("ms")) {
    159                 value *= 1000;
    160             } else if (unit.equals("f")) {
    161                 value = value / frameRate * 1000000;
    162             } else if (unit.equals("t")) {
    163                 value = value / tickRate * 1000000;
    164             }
    165             return (long)value;
    166         }
    167         throw new NumberFormatException("Malformed time expression : " + time);
    168     }
    169 
    170     /**
    171      * Applies <a href
    172      * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the
    173      * default space policy</a> to the given string.
    174      *
    175      * @param in A string to apply the policy.
    176      */
    177     public static String applyDefaultSpacePolicy(String in) {
    178         return applySpacePolicy(in, true);
    179     }
    180 
    181     /**
    182      * Applies the space policy to the given string. This applies <a href
    183      * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the
    184      * default space policy</a> with linefeed-treatment as treat-as-space
    185      * or preserve.
    186      *
    187      * @param in A string to apply the policy.
    188      * @param treatLfAsSpace Whether convert line feeds to spaces or not.
    189      */
    190     public static String applySpacePolicy(String in, boolean treatLfAsSpace) {
    191         // Removes CR followed by LF. ref:
    192         // http://www.w3.org/TR/xml/#sec-line-ends
    193         String crRemoved = in.replaceAll("\r\n", "\n");
    194         // Apply suppress-at-line-break="auto" and
    195         // white-space-treatment="ignore-if-surrounding-linefeed"
    196         String spacesNeighboringLfRemoved = crRemoved.replaceAll(" *\n *", "\n");
    197         // Apply linefeed-treatment="treat-as-space"
    198         String lfToSpace = treatLfAsSpace ? spacesNeighboringLfRemoved.replaceAll("\n", " ")
    199                 : spacesNeighboringLfRemoved;
    200         // Apply white-space-collapse="true"
    201         String spacesCollapsed = lfToSpace.replaceAll("[ \t\\x0B\f\r]+", " ");
    202         return spacesCollapsed;
    203     }
    204 
    205     /**
    206      * Returns the timed text for the given time period.
    207      *
    208      * @param root The root node of the TTML document.
    209      * @param startUs The start time of the time period in microsecond.
    210      * @param endUs The end time of the time period in microsecond.
    211      */
    212     public static String extractText(TtmlNode root, long startUs, long endUs) {
    213         StringBuilder text = new StringBuilder();
    214         extractText(root, startUs, endUs, text, false);
    215         return text.toString().replaceAll("\n$", "");
    216     }
    217 
    218     private static void extractText(TtmlNode node, long startUs, long endUs, StringBuilder out,
    219             boolean inPTag) {
    220         if (node.mName.equals(TtmlUtils.PCDATA) && inPTag) {
    221             out.append(node.mText);
    222         } else if (node.mName.equals(TtmlUtils.TAG_BR) && inPTag) {
    223             out.append("\n");
    224         } else if (node.mName.equals(TtmlUtils.TAG_METADATA)) {
    225             // do nothing.
    226         } else if (node.isActive(startUs, endUs)) {
    227             boolean pTag = node.mName.equals(TtmlUtils.TAG_P);
    228             int length = out.length();
    229             for (int i = 0; i < node.mChildren.size(); ++i) {
    230                 extractText(node.mChildren.get(i), startUs, endUs, out, pTag || inPTag);
    231             }
    232             if (pTag && length != out.length()) {
    233                 out.append("\n");
    234             }
    235         }
    236     }
    237 
    238     /**
    239      * Returns a TTML fragment string for the given time period.
    240      *
    241      * @param root The root node of the TTML document.
    242      * @param startUs The start time of the time period in microsecond.
    243      * @param endUs The end time of the time period in microsecond.
    244      */
    245     public static String extractTtmlFragment(TtmlNode root, long startUs, long endUs) {
    246         StringBuilder fragment = new StringBuilder();
    247         extractTtmlFragment(root, startUs, endUs, fragment);
    248         return fragment.toString();
    249     }
    250 
    251     private static void extractTtmlFragment(TtmlNode node, long startUs, long endUs,
    252             StringBuilder out) {
    253         if (node.mName.equals(TtmlUtils.PCDATA)) {
    254             out.append(node.mText);
    255         } else if (node.mName.equals(TtmlUtils.TAG_BR)) {
    256             out.append("<br/>");
    257         } else if (node.isActive(startUs, endUs)) {
    258             out.append("<");
    259             out.append(node.mName);
    260             out.append(node.mAttributes);
    261             out.append(">");
    262             for (int i = 0; i < node.mChildren.size(); ++i) {
    263                 extractTtmlFragment(node.mChildren.get(i), startUs, endUs, out);
    264             }
    265             out.append("</");
    266             out.append(node.mName);
    267             out.append(">");
    268         }
    269     }
    270 }
    271 
    272 /**
    273  * A container class which represents a cue in TTML.
    274  * @hide
    275  */
    276 class TtmlCue extends SubtitleTrack.Cue {
    277     public String mText;
    278     public String mTtmlFragment;
    279 
    280     public TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment) {
    281         this.mStartTimeMs = startTimeMs;
    282         this.mEndTimeMs = endTimeMs;
    283         this.mText = text;
    284         this.mTtmlFragment = ttmlFragment;
    285     }
    286 }
    287 
    288 /**
    289  * A container class which represents a node in TTML.
    290  *
    291  * @hide
    292  */
    293 class TtmlNode {
    294     public final String mName;
    295     public final String mAttributes;
    296     public final TtmlNode mParent;
    297     public final String mText;
    298     public final List<TtmlNode> mChildren = new ArrayList<TtmlNode>();
    299     public final long mRunId;
    300     public final long mStartTimeMs;
    301     public final long mEndTimeMs;
    302 
    303     public TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs,
    304             TtmlNode parent, long runId) {
    305         this.mName = name;
    306         this.mAttributes = attributes;
    307         this.mText = text;
    308         this.mStartTimeMs = startTimeMs;
    309         this.mEndTimeMs = endTimeMs;
    310         this.mParent = parent;
    311         this.mRunId = runId;
    312     }
    313 
    314     /**
    315      * Check if this node is active in the given time range.
    316      *
    317      * @param startTimeMs The start time of the range to check in microsecond.
    318      * @param endTimeMs The end time of the range to check in microsecond.
    319      * @return return true if the given range overlaps the time range of this
    320      *         node.
    321      */
    322     public boolean isActive(long startTimeMs, long endTimeMs) {
    323         return this.mEndTimeMs > startTimeMs && this.mStartTimeMs < endTimeMs;
    324     }
    325 }
    326 
    327 /**
    328  * A simple TTML parser (http://www.w3.org/TR/ttaf1-dfxp/) which supports DFXP
    329  * presentation profile.
    330  * <p>
    331  * Supported features in this parser are:
    332  * <ul>
    333  * <li>content
    334  * <li>core
    335  * <li>presentation
    336  * <li>profile
    337  * <li>structure
    338  * <li>time-offset
    339  * <li>timing
    340  * <li>tickRate
    341  * <li>time-clock-with-frames
    342  * <li>time-clock
    343  * <li>time-offset-with-frames
    344  * <li>time-offset-with-ticks
    345  * </ul>
    346  * </p>
    347  *
    348  * @hide
    349  */
    350 class TtmlParser {
    351     static final String TAG = "TtmlParser";
    352 
    353     // TODO: read and apply the following attributes if specified.
    354     private static final int DEFAULT_FRAMERATE = 30;
    355     private static final int DEFAULT_SUBFRAMERATE = 1;
    356     private static final int DEFAULT_TICKRATE = 1;
    357 
    358     private XmlPullParser mParser;
    359     private final TtmlNodeListener mListener;
    360     private long mCurrentRunId;
    361 
    362     public TtmlParser(TtmlNodeListener listener) {
    363         mListener = listener;
    364     }
    365 
    366     /**
    367      * Parse TTML data. Once this is called, all the previous data are
    368      * reset and it starts parsing for the given text.
    369      *
    370      * @param ttmlText TTML text to parse.
    371      * @throws XmlPullParserException
    372      * @throws IOException
    373      */
    374     public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException {
    375         mParser = null;
    376         mCurrentRunId = runId;
    377         loadParser(ttmlText);
    378         parseTtml();
    379     }
    380 
    381     private void loadParser(String ttmlFragment) throws XmlPullParserException {
    382         XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
    383         factory.setNamespaceAware(false);
    384         mParser = factory.newPullParser();
    385         StringReader in = new StringReader(ttmlFragment);
    386         mParser.setInput(in);
    387     }
    388 
    389     private void extractAttribute(XmlPullParser parser, int i, StringBuilder out) {
    390         out.append(" ");
    391         out.append(parser.getAttributeName(i));
    392         out.append("=\"");
    393         out.append(parser.getAttributeValue(i));
    394         out.append("\"");
    395     }
    396 
    397     private void parseTtml() throws XmlPullParserException, IOException {
    398         LinkedList<TtmlNode> nodeStack = new LinkedList<TtmlNode>();
    399         int depthInUnsupportedTag = 0;
    400         boolean active = true;
    401         while (!isEndOfDoc()) {
    402             int eventType = mParser.getEventType();
    403             TtmlNode parent = nodeStack.peekLast();
    404             if (active) {
    405                 if (eventType == XmlPullParser.START_TAG) {
    406                     if (!isSupportedTag(mParser.getName())) {
    407                         Log.w(TAG, "Unsupported tag " + mParser.getName() + " is ignored.");
    408                         depthInUnsupportedTag++;
    409                         active = false;
    410                     } else {
    411                         TtmlNode node = parseNode(parent);
    412                         nodeStack.addLast(node);
    413                         if (parent != null) {
    414                             parent.mChildren.add(node);
    415                         }
    416                     }
    417                 } else if (eventType == XmlPullParser.TEXT) {
    418                     String text = TtmlUtils.applyDefaultSpacePolicy(mParser.getText());
    419                     if (!TextUtils.isEmpty(text)) {
    420                         parent.mChildren.add(new TtmlNode(
    421                                 TtmlUtils.PCDATA, "", text, 0, TtmlUtils.INVALID_TIMESTAMP,
    422                                 parent, mCurrentRunId));
    423 
    424                     }
    425                 } else if (eventType == XmlPullParser.END_TAG) {
    426                     if (mParser.getName().equals(TtmlUtils.TAG_P)) {
    427                         mListener.onTtmlNodeParsed(nodeStack.getLast());
    428                     } else if (mParser.getName().equals(TtmlUtils.TAG_TT)) {
    429                         mListener.onRootNodeParsed(nodeStack.getLast());
    430                     }
    431                     nodeStack.removeLast();
    432                 }
    433             } else {
    434                 if (eventType == XmlPullParser.START_TAG) {
    435                     depthInUnsupportedTag++;
    436                 } else if (eventType == XmlPullParser.END_TAG) {
    437                     depthInUnsupportedTag--;
    438                     if (depthInUnsupportedTag == 0) {
    439                         active = true;
    440                     }
    441                 }
    442             }
    443             mParser.next();
    444         }
    445     }
    446 
    447     private TtmlNode parseNode(TtmlNode parent) throws XmlPullParserException, IOException {
    448         int eventType = mParser.getEventType();
    449         if (!(eventType == XmlPullParser.START_TAG)) {
    450             return null;
    451         }
    452         StringBuilder attrStr = new StringBuilder();
    453         long start = 0;
    454         long end = TtmlUtils.INVALID_TIMESTAMP;
    455         long dur = 0;
    456         for (int i = 0; i < mParser.getAttributeCount(); ++i) {
    457             String attr = mParser.getAttributeName(i);
    458             String value = mParser.getAttributeValue(i);
    459             // TODO: check if it's safe to ignore the namespace of attributes as follows.
    460             attr = attr.replaceFirst("^.*:", "");
    461             if (attr.equals(TtmlUtils.ATTR_BEGIN)) {
    462                 start = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE,
    463                         DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
    464             } else if (attr.equals(TtmlUtils.ATTR_END)) {
    465                 end = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE,
    466                         DEFAULT_TICKRATE);
    467             } else if (attr.equals(TtmlUtils.ATTR_DURATION)) {
    468                 dur = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE,
    469                         DEFAULT_TICKRATE);
    470             } else {
    471                 extractAttribute(mParser, i, attrStr);
    472             }
    473         }
    474         if (parent != null) {
    475             start += parent.mStartTimeMs;
    476             if (end != TtmlUtils.INVALID_TIMESTAMP) {
    477                 end += parent.mStartTimeMs;
    478             }
    479         }
    480         if (dur > 0) {
    481             if (end != TtmlUtils.INVALID_TIMESTAMP) {
    482                 Log.e(TAG, "'dur' and 'end' attributes are defined at the same time." +
    483                         "'end' value is ignored.");
    484             }
    485             end = start + dur;
    486         }
    487         if (parent != null) {
    488             // If the end time remains unspecified, then the end point is
    489             // interpreted as the end point of the external time interval.
    490             if (end == TtmlUtils.INVALID_TIMESTAMP &&
    491                     parent.mEndTimeMs != TtmlUtils.INVALID_TIMESTAMP &&
    492                     end > parent.mEndTimeMs) {
    493                 end = parent.mEndTimeMs;
    494             }
    495         }
    496         TtmlNode node = new TtmlNode(mParser.getName(), attrStr.toString(), null, start, end,
    497                 parent, mCurrentRunId);
    498         return node;
    499     }
    500 
    501     private boolean isEndOfDoc() throws XmlPullParserException {
    502         return (mParser.getEventType() == XmlPullParser.END_DOCUMENT);
    503     }
    504 
    505     private static boolean isSupportedTag(String tag) {
    506         if (tag.equals(TtmlUtils.TAG_TT) || tag.equals(TtmlUtils.TAG_HEAD) ||
    507                 tag.equals(TtmlUtils.TAG_BODY) || tag.equals(TtmlUtils.TAG_DIV) ||
    508                 tag.equals(TtmlUtils.TAG_P) || tag.equals(TtmlUtils.TAG_SPAN) ||
    509                 tag.equals(TtmlUtils.TAG_BR) || tag.equals(TtmlUtils.TAG_STYLE) ||
    510                 tag.equals(TtmlUtils.TAG_STYLING) || tag.equals(TtmlUtils.TAG_LAYOUT) ||
    511                 tag.equals(TtmlUtils.TAG_REGION) || tag.equals(TtmlUtils.TAG_METADATA) ||
    512                 tag.equals(TtmlUtils.TAG_SMPTE_IMAGE) || tag.equals(TtmlUtils.TAG_SMPTE_DATA) ||
    513                 tag.equals(TtmlUtils.TAG_SMPTE_INFORMATION)) {
    514             return true;
    515         }
    516         return false;
    517     }
    518 }
    519 
    520 /** @hide */
    521 interface TtmlNodeListener {
    522     void onTtmlNodeParsed(TtmlNode node);
    523     void onRootNodeParsed(TtmlNode node);
    524 }
    525 
    526 /** @hide */
    527 class TtmlTrack extends SubtitleTrack implements TtmlNodeListener {
    528     private static final String TAG = "TtmlTrack";
    529 
    530     private final TtmlParser mParser = new TtmlParser(this);
    531     private final TtmlRenderingWidget mRenderingWidget;
    532     private String mParsingData;
    533     private Long mCurrentRunID;
    534 
    535     private final LinkedList<TtmlNode> mTtmlNodes;
    536     private final TreeSet<Long> mTimeEvents;
    537     private TtmlNode mRootNode;
    538 
    539     TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format) {
    540         super(format);
    541 
    542         mTtmlNodes = new LinkedList<TtmlNode>();
    543         mTimeEvents = new TreeSet<Long>();
    544         mRenderingWidget = renderingWidget;
    545         mParsingData = "";
    546     }
    547 
    548     @Override
    549     public TtmlRenderingWidget getRenderingWidget() {
    550         return mRenderingWidget;
    551     }
    552 
    553     @Override
    554     public void onData(byte[] data, boolean eos, long runID) {
    555         try {
    556             // TODO: handle UTF-8 conversion properly
    557             String str = new String(data, "UTF-8");
    558 
    559             // implement intermixing restriction for TTML.
    560             synchronized(mParser) {
    561                 if (mCurrentRunID != null && runID != mCurrentRunID) {
    562                     throw new IllegalStateException(
    563                             "Run #" + mCurrentRunID +
    564                             " in progress.  Cannot process run #" + runID);
    565                 }
    566                 mCurrentRunID = runID;
    567                 mParsingData += str;
    568                 if (eos) {
    569                     try {
    570                         mParser.parse(mParsingData, mCurrentRunID);
    571                     } catch (XmlPullParserException e) {
    572                         e.printStackTrace();
    573                     } catch (IOException e) {
    574                         e.printStackTrace();
    575                     }
    576                     finishedRun(runID);
    577                     mParsingData = "";
    578                     mCurrentRunID = null;
    579                 }
    580             }
    581         } catch (java.io.UnsupportedEncodingException e) {
    582             Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
    583         }
    584     }
    585 
    586     @Override
    587     public void onTtmlNodeParsed(TtmlNode node) {
    588         mTtmlNodes.addLast(node);
    589         addTimeEvents(node);
    590     }
    591 
    592     @Override
    593     public void onRootNodeParsed(TtmlNode node) {
    594         mRootNode = node;
    595         TtmlCue cue = null;
    596         while ((cue = getNextResult()) != null) {
    597             addCue(cue);
    598         }
    599         mRootNode = null;
    600         mTtmlNodes.clear();
    601         mTimeEvents.clear();
    602     }
    603 
    604     @Override
    605     public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
    606         if (!mVisible) {
    607             // don't keep the state if we are not visible
    608             return;
    609         }
    610 
    611         if (DEBUG && mTimeProvider != null) {
    612             try {
    613                 Log.d(TAG, "at " +
    614                         (mTimeProvider.getCurrentTimeUs(false, true) / 1000) +
    615                         " ms the active cues are:");
    616             } catch (IllegalStateException e) {
    617                 Log.d(TAG, "at (illegal state) the active cues are:");
    618             }
    619         }
    620 
    621         mRenderingWidget.setActiveCues(activeCues);
    622     }
    623 
    624     /**
    625      * Returns a {@link TtmlCue} in the presentation time order.
    626      * {@code null} is returned if there is no more timed text to show.
    627      */
    628     public TtmlCue getNextResult() {
    629         while (mTimeEvents.size() >= 2) {
    630             long start = mTimeEvents.pollFirst();
    631             long end = mTimeEvents.first();
    632             List<TtmlNode> activeCues = getActiveNodes(start, end);
    633             if (!activeCues.isEmpty()) {
    634                 return new TtmlCue(start, end,
    635                         TtmlUtils.applySpacePolicy(TtmlUtils.extractText(
    636                                 mRootNode, start, end), false),
    637                         TtmlUtils.extractTtmlFragment(mRootNode, start, end));
    638             }
    639         }
    640         return null;
    641     }
    642 
    643     private void addTimeEvents(TtmlNode node) {
    644         mTimeEvents.add(node.mStartTimeMs);
    645         mTimeEvents.add(node.mEndTimeMs);
    646         for (int i = 0; i < node.mChildren.size(); ++i) {
    647             addTimeEvents(node.mChildren.get(i));
    648         }
    649     }
    650 
    651     private List<TtmlNode> getActiveNodes(long startTimeUs, long endTimeUs) {
    652         List<TtmlNode> activeNodes = new ArrayList<TtmlNode>();
    653         for (int i = 0; i < mTtmlNodes.size(); ++i) {
    654             TtmlNode node = mTtmlNodes.get(i);
    655             if (node.isActive(startTimeUs, endTimeUs)) {
    656                 activeNodes.add(node);
    657             }
    658         }
    659         return activeNodes;
    660     }
    661 }
    662 
    663 /**
    664  * Widget capable of rendering TTML captions.
    665  *
    666  * @hide
    667  */
    668 class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget {
    669 
    670     /** Callback for rendering changes. */
    671     private OnChangedListener mListener;
    672     private final TextView mTextView;
    673 
    674     public TtmlRenderingWidget(Context context) {
    675         this(context, null);
    676     }
    677 
    678     public TtmlRenderingWidget(Context context, AttributeSet attrs) {
    679         this(context, attrs, 0);
    680     }
    681 
    682     public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
    683         this(context, attrs, defStyleAttr, 0);
    684     }
    685 
    686     public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr,
    687             int defStyleRes) {
    688         super(context, attrs, defStyleAttr, defStyleRes);
    689         // Cannot render text over video when layer type is hardware.
    690         setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    691 
    692         CaptioningManager captionManager = (CaptioningManager) context.getSystemService(
    693                 Context.CAPTIONING_SERVICE);
    694         mTextView = new TextView(context);
    695         mTextView.setTextColor(captionManager.getUserStyle().foregroundColor);
    696         addView(mTextView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    697         mTextView.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
    698     }
    699 
    700     @Override
    701     public void setOnChangedListener(OnChangedListener listener) {
    702         mListener = listener;
    703     }
    704 
    705     @Override
    706     public void setSize(int width, int height) {
    707         final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    708         final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    709 
    710         measure(widthSpec, heightSpec);
    711         layout(0, 0, width, height);
    712     }
    713 
    714     @Override
    715     public void setVisible(boolean visible) {
    716         if (visible) {
    717             setVisibility(View.VISIBLE);
    718         } else {
    719             setVisibility(View.GONE);
    720         }
    721     }
    722 
    723     @Override
    724     public void onAttachedToWindow() {
    725         super.onAttachedToWindow();
    726     }
    727 
    728     @Override
    729     public void onDetachedFromWindow() {
    730         super.onDetachedFromWindow();
    731     }
    732 
    733     public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
    734         final int count = activeCues.size();
    735         String subtitleText = "";
    736         for (int i = 0; i < count; i++) {
    737             TtmlCue cue = (TtmlCue) activeCues.get(i);
    738             subtitleText += cue.mText + "\n";
    739         }
    740         mTextView.setText(subtitleText);
    741 
    742         if (mListener != null) {
    743             mListener.onChanged(this);
    744         }
    745     }
    746 }
    747