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