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 
TtmlRenderer(Context context)51     public TtmlRenderer(Context context) {
52         mContext = context;
53     }
54 
55     @Override
supports(MediaFormat format)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
createTrack(MediaFormat format)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 
TtmlUtils()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      */
parseTimeExpression(String time, int frameRate, int subframeRate, int tickRate)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      */
applyDefaultSpacePolicy(String in)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      */
applySpacePolicy(String in, boolean treatLfAsSpace)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      */
extractText(TtmlNode root, long startUs, long endUs)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 
extractText(TtmlNode node, long startUs, long endUs, StringBuilder out, boolean inPTag)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      */
extractTtmlFragment(TtmlNode root, long startUs, long endUs)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 
extractTtmlFragment(TtmlNode node, long startUs, long endUs, StringBuilder out)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 
TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment)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 
TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs, TtmlNode parent, long runId)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      */
isActive(long startTimeMs, long endTimeMs)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 
TtmlParser(TtmlNodeListener listener)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      */
parse(String ttmlText, long runId)374     public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException {
375         mParser = null;
376         mCurrentRunId = runId;
377         loadParser(ttmlText);
378         parseTtml();
379     }
380 
loadParser(String ttmlFragment)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 
extractAttribute(XmlPullParser parser, int i, StringBuilder out)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 
parseTtml()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 
parseNode(TtmlNode parent)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 
isEndOfDoc()501     private boolean isEndOfDoc() throws XmlPullParserException {
502         return (mParser.getEventType() == XmlPullParser.END_DOCUMENT);
503     }
504 
isSupportedTag(String tag)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 {
onTtmlNodeParsed(TtmlNode node)522     void onTtmlNodeParsed(TtmlNode node);
onRootNodeParsed(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 
TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format)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
getRenderingWidget()549     public TtmlRenderingWidget getRenderingWidget() {
550         return mRenderingWidget;
551     }
552 
553     @Override
onData(byte[] data, boolean eos, long runID)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
onTtmlNodeParsed(TtmlNode node)587     public void onTtmlNodeParsed(TtmlNode node) {
588         mTtmlNodes.addLast(node);
589         addTimeEvents(node);
590     }
591 
592     @Override
onRootNodeParsed(TtmlNode node)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
updateView(Vector<SubtitleTrack.Cue> activeCues)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      */
getNextResult()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 
addTimeEvents(TtmlNode node)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 
getActiveNodes(long startTimeUs, long endTimeUs)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 
TtmlRenderingWidget(Context context)674     public TtmlRenderingWidget(Context context) {
675         this(context, null);
676     }
677 
TtmlRenderingWidget(Context context, AttributeSet attrs)678     public TtmlRenderingWidget(Context context, AttributeSet attrs) {
679         this(context, attrs, 0);
680     }
681 
TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr)682     public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
683         this(context, attrs, defStyleAttr, 0);
684     }
685 
TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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
setOnChangedListener(OnChangedListener listener)701     public void setOnChangedListener(OnChangedListener listener) {
702         mListener = listener;
703     }
704 
705     @Override
setSize(int width, int height)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
setVisible(boolean visible)715     public void setVisible(boolean visible) {
716         if (visible) {
717             setVisibility(View.VISIBLE);
718         } else {
719             setVisibility(View.GONE);
720         }
721     }
722 
723     @Override
onAttachedToWindow()724     public void onAttachedToWindow() {
725         super.onAttachedToWindow();
726     }
727 
728     @Override
onDetachedFromWindow()729     public void onDetachedFromWindow() {
730         super.onDetachedFromWindow();
731     }
732 
setActiveCues(Vector<SubtitleTrack.Cue> activeCues)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