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