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