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 17package android.media; 18 19import android.content.Context; 20import android.text.TextUtils; 21import android.util.AttributeSet; 22import android.util.Log; 23import android.view.Gravity; 24import android.view.View; 25import android.view.accessibility.CaptioningManager; 26import android.widget.LinearLayout; 27import android.widget.TextView; 28 29import java.io.IOException; 30import java.io.StringReader; 31import java.util.ArrayList; 32import java.util.LinkedList; 33import java.util.List; 34import java.util.TreeSet; 35import java.util.Vector; 36import java.util.regex.Matcher; 37import java.util.regex.Pattern; 38 39import org.xmlpull.v1.XmlPullParser; 40import org.xmlpull.v1.XmlPullParserException; 41import org.xmlpull.v1.XmlPullParserFactory; 42 43/** @hide */ 44public 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 51 public TtmlRenderer(Context context) { 52 mContext = context; 53 } 54 55 @Override 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 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 */ 77final 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 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 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 */ 276class TtmlCue extends SubtitleTrack.Cue { 277 public String mText; 278 public String mTtmlFragment; 279 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 */ 293class 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 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 */ 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 */ 350class 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 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 */ 374 public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException { 375 mParser = null; 376 mCurrentRunId = runId; 377 loadParser(ttmlText); 378 parseTtml(); 379 } 380 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 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 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 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 501 private boolean isEndOfDoc() throws XmlPullParserException { 502 return (mParser.getEventType() == XmlPullParser.END_DOCUMENT); 503 } 504 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 */ 521interface TtmlNodeListener { 522 void onTtmlNodeParsed(TtmlNode node); 523 void onRootNodeParsed(TtmlNode node); 524} 525 526/** @hide */ 527class 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 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 549 public TtmlRenderingWidget getRenderingWidget() { 550 return mRenderingWidget; 551 } 552 553 @Override 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 587 public void onTtmlNodeParsed(TtmlNode node) { 588 mTtmlNodes.addLast(node); 589 addTimeEvents(node); 590 } 591 592 @Override 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 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 */ 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 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 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 */ 668class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget { 669 670 /** Callback for rendering changes. */ 671 private OnChangedListener mListener; 672 private final TextView mTextView; 673 674 public TtmlRenderingWidget(Context context) { 675 this(context, null); 676 } 677 678 public TtmlRenderingWidget(Context context, AttributeSet attrs) { 679 this(context, attrs, 0); 680 } 681 682 public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) { 683 this(context, attrs, defStyleAttr, 0); 684 } 685 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 701 public void setOnChangedListener(OnChangedListener listener) { 702 mListener = listener; 703 } 704 705 @Override 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 715 public void setVisible(boolean visible) { 716 if (visible) { 717 setVisibility(View.VISIBLE); 718 } else { 719 setVisibility(View.GONE); 720 } 721 } 722 723 @Override 724 public void onAttachedToWindow() { 725 super.onAttachedToWindow(); 726 } 727 728 @Override 729 public void onDetachedFromWindow() { 730 super.onDetachedFromWindow(); 731 } 732 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