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.text.TextUtils; 21 import android.util.AttributeSet; 22 import android.util.Log; 23 import android.view.Gravity; 24 import android.view.View; 25 import android.view.accessibility.CaptioningManager; 26 import android.widget.LinearLayout; 27 import android.widget.TextView; 28 29 import java.io.IOException; 30 import java.io.StringReader; 31 import java.util.ArrayList; 32 import java.util.LinkedList; 33 import java.util.List; 34 import java.util.TreeSet; 35 import java.util.Vector; 36 import java.util.regex.Matcher; 37 import java.util.regex.Pattern; 38 39 import org.xmlpull.v1.XmlPullParser; 40 import org.xmlpull.v1.XmlPullParserException; 41 import org.xmlpull.v1.XmlPullParserFactory; 42 43 /** @hide */ 44 public 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 TtmlRenderer(Context context)51 public TtmlRenderer(Context context) { 52 mContext = context; 53 } 54 55 @Override supports(MediaFormat format)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 createTrack(MediaFormat format)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 */ 77 final 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 TtmlUtils()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 */ parseTimeExpression(String time, int frameRate, int subframeRate, int tickRate)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 */ applyDefaultSpacePolicy(String in)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 */ applySpacePolicy(String in, boolean treatLfAsSpace)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 */ extractText(TtmlNode root, long startUs, long endUs)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 extractText(TtmlNode node, long startUs, long endUs, StringBuilder out, boolean inPTag)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 */ extractTtmlFragment(TtmlNode root, long startUs, long endUs)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 extractTtmlFragment(TtmlNode node, long startUs, long endUs, StringBuilder out)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 */ 276 class TtmlCue extends SubtitleTrack.Cue { 277 public String mText; 278 public String mTtmlFragment; 279 TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment)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 */ 293 class 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 TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs, TtmlNode parent, long runId)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 */ isActive(long startTimeMs, long endTimeMs)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 */ 350 class 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 TtmlParser(TtmlNodeListener listener)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 */ parse(String ttmlText, long runId)374 public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException { 375 mParser = null; 376 mCurrentRunId = runId; 377 loadParser(ttmlText); 378 parseTtml(); 379 } 380 loadParser(String ttmlFragment)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 extractAttribute(XmlPullParser parser, int i, StringBuilder out)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 parseTtml()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 parseNode(TtmlNode parent)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 isEndOfDoc()501 private boolean isEndOfDoc() throws XmlPullParserException { 502 return (mParser.getEventType() == XmlPullParser.END_DOCUMENT); 503 } 504 isSupportedTag(String tag)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 */ 521 interface TtmlNodeListener { onTtmlNodeParsed(TtmlNode node)522 void onTtmlNodeParsed(TtmlNode node); onRootNodeParsed(TtmlNode node)523 void onRootNodeParsed(TtmlNode node); 524 } 525 526 /** @hide */ 527 class 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 TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format)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 getRenderingWidget()549 public TtmlRenderingWidget getRenderingWidget() { 550 return mRenderingWidget; 551 } 552 553 @Override onData(byte[] data, boolean eos, long runID)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 onTtmlNodeParsed(TtmlNode node)587 public void onTtmlNodeParsed(TtmlNode node) { 588 mTtmlNodes.addLast(node); 589 addTimeEvents(node); 590 } 591 592 @Override onRootNodeParsed(TtmlNode node)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 updateView(Vector<SubtitleTrack.Cue> activeCues)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 */ getNextResult()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 addTimeEvents(TtmlNode node)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 getActiveNodes(long startTimeUs, long endTimeUs)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 */ 668 class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget { 669 670 /** Callback for rendering changes. */ 671 private OnChangedListener mListener; 672 private final TextView mTextView; 673 TtmlRenderingWidget(Context context)674 public TtmlRenderingWidget(Context context) { 675 this(context, null); 676 } 677 TtmlRenderingWidget(Context context, AttributeSet attrs)678 public TtmlRenderingWidget(Context context, AttributeSet attrs) { 679 this(context, attrs, 0); 680 } 681 TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr)682 public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) { 683 this(context, attrs, defStyleAttr, 0); 684 } 685 TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 setOnChangedListener(OnChangedListener listener)701 public void setOnChangedListener(OnChangedListener listener) { 702 mListener = listener; 703 } 704 705 @Override setSize(int width, int height)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 setVisible(boolean visible)715 public void setVisible(boolean visible) { 716 if (visible) { 717 setVisibility(View.VISIBLE); 718 } else { 719 setVisibility(View.GONE); 720 } 721 } 722 723 @Override onAttachedToWindow()724 public void onAttachedToWindow() { 725 super.onAttachedToWindow(); 726 } 727 728 @Override onDetachedFromWindow()729 public void onDetachedFromWindow() { 730 super.onDetachedFromWindow(); 731 } 732 setActiveCues(Vector<SubtitleTrack.Cue> activeCues)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