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