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