| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.media; |
| |
| import android.content.Context; |
| import android.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.accessibility.CaptioningManager; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.util.ArrayList; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.TreeSet; |
| import java.util.Vector; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlPullParserFactory; |
| |
| /** @hide */ |
| public class TtmlRenderer extends SubtitleController.Renderer { |
| private final Context mContext; |
| |
| private static final String MEDIA_MIMETYPE_TEXT_TTML = "application/ttml+xml"; |
| |
| private TtmlRenderingWidget mRenderingWidget; |
| |
| public TtmlRenderer(Context context) { |
| mContext = context; |
| } |
| |
| @Override |
| public boolean supports(MediaFormat format) { |
| if (format.containsKey(MediaFormat.KEY_MIME)) { |
| return format.getString(MediaFormat.KEY_MIME).equals(MEDIA_MIMETYPE_TEXT_TTML); |
| } |
| return false; |
| } |
| |
| @Override |
| public SubtitleTrack createTrack(MediaFormat format) { |
| if (mRenderingWidget == null) { |
| mRenderingWidget = new TtmlRenderingWidget(mContext); |
| } |
| return new TtmlTrack(mRenderingWidget, format); |
| } |
| } |
| |
| /** |
| * A class which provides utillity methods for TTML parsing. |
| * |
| * @hide |
| */ |
| final class TtmlUtils { |
| public static final String TAG_TT = "tt"; |
| public static final String TAG_HEAD = "head"; |
| public static final String TAG_BODY = "body"; |
| public static final String TAG_DIV = "div"; |
| public static final String TAG_P = "p"; |
| public static final String TAG_SPAN = "span"; |
| public static final String TAG_BR = "br"; |
| public static final String TAG_STYLE = "style"; |
| public static final String TAG_STYLING = "styling"; |
| public static final String TAG_LAYOUT = "layout"; |
| public static final String TAG_REGION = "region"; |
| public static final String TAG_METADATA = "metadata"; |
| public static final String TAG_SMPTE_IMAGE = "smpte:image"; |
| public static final String TAG_SMPTE_DATA = "smpte:data"; |
| public static final String TAG_SMPTE_INFORMATION = "smpte:information"; |
| public static final String PCDATA = "#pcdata"; |
| public static final String ATTR_BEGIN = "begin"; |
| public static final String ATTR_DURATION = "dur"; |
| public static final String ATTR_END = "end"; |
| public static final long INVALID_TIMESTAMP = Long.MAX_VALUE; |
| |
| /** |
| * Time expression RE according to the spec: |
| * http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression |
| */ |
| private static final Pattern CLOCK_TIME = Pattern.compile( |
| "^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])" |
| + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$"); |
| |
| private static final Pattern OFFSET_TIME = Pattern.compile( |
| "^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$"); |
| |
| private TtmlUtils() { |
| } |
| |
| /** |
| * Parses the given time expression and returns a timestamp in millisecond. |
| * <p> |
| * For the format of the time expression, please refer <a href= |
| * "http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a> |
| * |
| * @param time A string which includes time expression. |
| * @param frameRate the framerate of the stream. |
| * @param subframeRate the sub-framerate of the stream |
| * @param tickRate the tick rate of the stream. |
| * @return the parsed timestamp in micro-second. |
| * @throws NumberFormatException if the given string does not match to the |
| * format. |
| */ |
| public static long parseTimeExpression(String time, int frameRate, int subframeRate, |
| int tickRate) throws NumberFormatException { |
| Matcher matcher = CLOCK_TIME.matcher(time); |
| if (matcher.matches()) { |
| String hours = matcher.group(1); |
| double durationSeconds = Long.parseLong(hours) * 3600; |
| String minutes = matcher.group(2); |
| durationSeconds += Long.parseLong(minutes) * 60; |
| String seconds = matcher.group(3); |
| durationSeconds += Long.parseLong(seconds); |
| String fraction = matcher.group(4); |
| durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0; |
| String frames = matcher.group(5); |
| durationSeconds += (frames != null) ? ((double)Long.parseLong(frames)) / frameRate : 0; |
| String subframes = matcher.group(6); |
| durationSeconds += (subframes != null) ? ((double)Long.parseLong(subframes)) |
| / subframeRate / frameRate |
| : 0; |
| return (long)(durationSeconds * 1000); |
| } |
| matcher = OFFSET_TIME.matcher(time); |
| if (matcher.matches()) { |
| String timeValue = matcher.group(1); |
| double value = Double.parseDouble(timeValue); |
| String unit = matcher.group(2); |
| if (unit.equals("h")) { |
| value *= 3600L * 1000000L; |
| } else if (unit.equals("m")) { |
| value *= 60 * 1000000; |
| } else if (unit.equals("s")) { |
| value *= 1000000; |
| } else if (unit.equals("ms")) { |
| value *= 1000; |
| } else if (unit.equals("f")) { |
| value = value / frameRate * 1000000; |
| } else if (unit.equals("t")) { |
| value = value / tickRate * 1000000; |
| } |
| return (long)value; |
| } |
| throw new NumberFormatException("Malformed time expression : " + time); |
| } |
| |
| /** |
| * Applies <a href |
| * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the |
| * default space policy</a> to the given string. |
| * |
| * @param in A string to apply the policy. |
| */ |
| public static String applyDefaultSpacePolicy(String in) { |
| return applySpacePolicy(in, true); |
| } |
| |
| /** |
| * Applies the space policy to the given string. This applies <a href |
| * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the |
| * default space policy</a> with linefeed-treatment as treat-as-space |
| * or preserve. |
| * |
| * @param in A string to apply the policy. |
| * @param treatLfAsSpace Whether convert line feeds to spaces or not. |
| */ |
| public static String applySpacePolicy(String in, boolean treatLfAsSpace) { |
| // Removes CR followed by LF. ref: |
| // http://www.w3.org/TR/xml/#sec-line-ends |
| String crRemoved = in.replaceAll("\r\n", "\n"); |
| // Apply suppress-at-line-break="auto" and |
| // white-space-treatment="ignore-if-surrounding-linefeed" |
| String spacesNeighboringLfRemoved = crRemoved.replaceAll(" *\n *", "\n"); |
| // Apply linefeed-treatment="treat-as-space" |
| String lfToSpace = treatLfAsSpace ? spacesNeighboringLfRemoved.replaceAll("\n", " ") |
| : spacesNeighboringLfRemoved; |
| // Apply white-space-collapse="true" |
| String spacesCollapsed = lfToSpace.replaceAll("[ \t\\x0B\f\r]+", " "); |
| return spacesCollapsed; |
| } |
| |
| /** |
| * Returns the timed text for the given time period. |
| * |
| * @param root The root node of the TTML document. |
| * @param startUs The start time of the time period in microsecond. |
| * @param endUs The end time of the time period in microsecond. |
| */ |
| public static String extractText(TtmlNode root, long startUs, long endUs) { |
| StringBuilder text = new StringBuilder(); |
| extractText(root, startUs, endUs, text, false); |
| return text.toString().replaceAll("\n$", ""); |
| } |
| |
| private static void extractText(TtmlNode node, long startUs, long endUs, StringBuilder out, |
| boolean inPTag) { |
| if (node.mName.equals(TtmlUtils.PCDATA) && inPTag) { |
| out.append(node.mText); |
| } else if (node.mName.equals(TtmlUtils.TAG_BR) && inPTag) { |
| out.append("\n"); |
| } else if (node.mName.equals(TtmlUtils.TAG_METADATA)) { |
| // do nothing. |
| } else if (node.isActive(startUs, endUs)) { |
| boolean pTag = node.mName.equals(TtmlUtils.TAG_P); |
| int length = out.length(); |
| for (int i = 0; i < node.mChildren.size(); ++i) { |
| extractText(node.mChildren.get(i), startUs, endUs, out, pTag || inPTag); |
| } |
| if (pTag && length != out.length()) { |
| out.append("\n"); |
| } |
| } |
| } |
| |
| /** |
| * Returns a TTML fragment string for the given time period. |
| * |
| * @param root The root node of the TTML document. |
| * @param startUs The start time of the time period in microsecond. |
| * @param endUs The end time of the time period in microsecond. |
| */ |
| public static String extractTtmlFragment(TtmlNode root, long startUs, long endUs) { |
| StringBuilder fragment = new StringBuilder(); |
| extractTtmlFragment(root, startUs, endUs, fragment); |
| return fragment.toString(); |
| } |
| |
| private static void extractTtmlFragment(TtmlNode node, long startUs, long endUs, |
| StringBuilder out) { |
| if (node.mName.equals(TtmlUtils.PCDATA)) { |
| out.append(node.mText); |
| } else if (node.mName.equals(TtmlUtils.TAG_BR)) { |
| out.append("<br/>"); |
| } else if (node.isActive(startUs, endUs)) { |
| out.append("<"); |
| out.append(node.mName); |
| out.append(node.mAttributes); |
| out.append(">"); |
| for (int i = 0; i < node.mChildren.size(); ++i) { |
| extractTtmlFragment(node.mChildren.get(i), startUs, endUs, out); |
| } |
| out.append("</"); |
| out.append(node.mName); |
| out.append(">"); |
| } |
| } |
| } |
| |
| /** |
| * A container class which represents a cue in TTML. |
| * @hide |
| */ |
| class TtmlCue extends SubtitleTrack.Cue { |
| public String mText; |
| public String mTtmlFragment; |
| |
| public TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment) { |
| this.mStartTimeMs = startTimeMs; |
| this.mEndTimeMs = endTimeMs; |
| this.mText = text; |
| this.mTtmlFragment = ttmlFragment; |
| } |
| } |
| |
| /** |
| * A container class which represents a node in TTML. |
| * |
| * @hide |
| */ |
| class TtmlNode { |
| public final String mName; |
| public final String mAttributes; |
| public final TtmlNode mParent; |
| public final String mText; |
| public final List<TtmlNode> mChildren = new ArrayList<TtmlNode>(); |
| public final long mRunId; |
| public final long mStartTimeMs; |
| public final long mEndTimeMs; |
| |
| public TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs, |
| TtmlNode parent, long runId) { |
| this.mName = name; |
| this.mAttributes = attributes; |
| this.mText = text; |
| this.mStartTimeMs = startTimeMs; |
| this.mEndTimeMs = endTimeMs; |
| this.mParent = parent; |
| this.mRunId = runId; |
| } |
| |
| /** |
| * Check if this node is active in the given time range. |
| * |
| * @param startTimeMs The start time of the range to check in microsecond. |
| * @param endTimeMs The end time of the range to check in microsecond. |
| * @return return true if the given range overlaps the time range of this |
| * node. |
| */ |
| public boolean isActive(long startTimeMs, long endTimeMs) { |
| return this.mEndTimeMs > startTimeMs && this.mStartTimeMs < endTimeMs; |
| } |
| } |
| |
| /** |
| * A simple TTML parser (http://www.w3.org/TR/ttaf1-dfxp/) which supports DFXP |
| * presentation profile. |
| * <p> |
| * Supported features in this parser are: |
| * <ul> |
| * <li>content |
| * <li>core |
| * <li>presentation |
| * <li>profile |
| * <li>structure |
| * <li>time-offset |
| * <li>timing |
| * <li>tickRate |
| * <li>time-clock-with-frames |
| * <li>time-clock |
| * <li>time-offset-with-frames |
| * <li>time-offset-with-ticks |
| * </ul> |
| * </p> |
| * |
| * @hide |
| */ |
| class TtmlParser { |
| static final String TAG = "TtmlParser"; |
| |
| // TODO: read and apply the following attributes if specified. |
| private static final int DEFAULT_FRAMERATE = 30; |
| private static final int DEFAULT_SUBFRAMERATE = 1; |
| private static final int DEFAULT_TICKRATE = 1; |
| |
| private XmlPullParser mParser; |
| private final TtmlNodeListener mListener; |
| private long mCurrentRunId; |
| |
| public TtmlParser(TtmlNodeListener listener) { |
| mListener = listener; |
| } |
| |
| /** |
| * Parse TTML data. Once this is called, all the previous data are |
| * reset and it starts parsing for the given text. |
| * |
| * @param ttmlText TTML text to parse. |
| * @throws XmlPullParserException |
| * @throws IOException |
| */ |
| public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException { |
| mParser = null; |
| mCurrentRunId = runId; |
| loadParser(ttmlText); |
| parseTtml(); |
| } |
| |
| private void loadParser(String ttmlFragment) throws XmlPullParserException { |
| XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); |
| factory.setNamespaceAware(false); |
| mParser = factory.newPullParser(); |
| StringReader in = new StringReader(ttmlFragment); |
| mParser.setInput(in); |
| } |
| |
| private void extractAttribute(XmlPullParser parser, int i, StringBuilder out) { |
| out.append(" "); |
| out.append(parser.getAttributeName(i)); |
| out.append("=\""); |
| out.append(parser.getAttributeValue(i)); |
| out.append("\""); |
| } |
| |
| private void parseTtml() throws XmlPullParserException, IOException { |
| LinkedList<TtmlNode> nodeStack = new LinkedList<TtmlNode>(); |
| int depthInUnsupportedTag = 0; |
| boolean active = true; |
| while (!isEndOfDoc()) { |
| int eventType = mParser.getEventType(); |
| TtmlNode parent = nodeStack.peekLast(); |
| if (active) { |
| if (eventType == XmlPullParser.START_TAG) { |
| if (!isSupportedTag(mParser.getName())) { |
| Log.w(TAG, "Unsupported tag " + mParser.getName() + " is ignored."); |
| depthInUnsupportedTag++; |
| active = false; |
| } else { |
| TtmlNode node = parseNode(parent); |
| nodeStack.addLast(node); |
| if (parent != null) { |
| parent.mChildren.add(node); |
| } |
| } |
| } else if (eventType == XmlPullParser.TEXT) { |
| String text = TtmlUtils.applyDefaultSpacePolicy(mParser.getText()); |
| if (!TextUtils.isEmpty(text)) { |
| parent.mChildren.add(new TtmlNode( |
| TtmlUtils.PCDATA, "", text, 0, TtmlUtils.INVALID_TIMESTAMP, |
| parent, mCurrentRunId)); |
| |
| } |
| } else if (eventType == XmlPullParser.END_TAG) { |
| if (mParser.getName().equals(TtmlUtils.TAG_P)) { |
| mListener.onTtmlNodeParsed(nodeStack.getLast()); |
| } else if (mParser.getName().equals(TtmlUtils.TAG_TT)) { |
| mListener.onRootNodeParsed(nodeStack.getLast()); |
| } |
| nodeStack.removeLast(); |
| } |
| } else { |
| if (eventType == XmlPullParser.START_TAG) { |
| depthInUnsupportedTag++; |
| } else if (eventType == XmlPullParser.END_TAG) { |
| depthInUnsupportedTag--; |
| if (depthInUnsupportedTag == 0) { |
| active = true; |
| } |
| } |
| } |
| mParser.next(); |
| } |
| } |
| |
| private TtmlNode parseNode(TtmlNode parent) throws XmlPullParserException, IOException { |
| int eventType = mParser.getEventType(); |
| if (!(eventType == XmlPullParser.START_TAG)) { |
| return null; |
| } |
| StringBuilder attrStr = new StringBuilder(); |
| long start = 0; |
| long end = TtmlUtils.INVALID_TIMESTAMP; |
| long dur = 0; |
| for (int i = 0; i < mParser.getAttributeCount(); ++i) { |
| String attr = mParser.getAttributeName(i); |
| String value = mParser.getAttributeValue(i); |
| // TODO: check if it's safe to ignore the namespace of attributes as follows. |
| attr = attr.replaceFirst("^.*:", ""); |
| if (attr.equals(TtmlUtils.ATTR_BEGIN)) { |
| start = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, |
| DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE); |
| } else if (attr.equals(TtmlUtils.ATTR_END)) { |
| end = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, |
| DEFAULT_TICKRATE); |
| } else if (attr.equals(TtmlUtils.ATTR_DURATION)) { |
| dur = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, |
| DEFAULT_TICKRATE); |
| } else { |
| extractAttribute(mParser, i, attrStr); |
| } |
| } |
| if (parent != null) { |
| start += parent.mStartTimeMs; |
| if (end != TtmlUtils.INVALID_TIMESTAMP) { |
| end += parent.mStartTimeMs; |
| } |
| } |
| if (dur > 0) { |
| if (end != TtmlUtils.INVALID_TIMESTAMP) { |
| Log.e(TAG, "'dur' and 'end' attributes are defined at the same time." + |
| "'end' value is ignored."); |
| } |
| end = start + dur; |
| } |
| if (parent != null) { |
| // If the end time remains unspecified, then the end point is |
| // interpreted as the end point of the external time interval. |
| if (end == TtmlUtils.INVALID_TIMESTAMP && |
| parent.mEndTimeMs != TtmlUtils.INVALID_TIMESTAMP && |
| end > parent.mEndTimeMs) { |
| end = parent.mEndTimeMs; |
| } |
| } |
| TtmlNode node = new TtmlNode(mParser.getName(), attrStr.toString(), null, start, end, |
| parent, mCurrentRunId); |
| return node; |
| } |
| |
| private boolean isEndOfDoc() throws XmlPullParserException { |
| return (mParser.getEventType() == XmlPullParser.END_DOCUMENT); |
| } |
| |
| private static boolean isSupportedTag(String tag) { |
| if (tag.equals(TtmlUtils.TAG_TT) || tag.equals(TtmlUtils.TAG_HEAD) || |
| tag.equals(TtmlUtils.TAG_BODY) || tag.equals(TtmlUtils.TAG_DIV) || |
| tag.equals(TtmlUtils.TAG_P) || tag.equals(TtmlUtils.TAG_SPAN) || |
| tag.equals(TtmlUtils.TAG_BR) || tag.equals(TtmlUtils.TAG_STYLE) || |
| tag.equals(TtmlUtils.TAG_STYLING) || tag.equals(TtmlUtils.TAG_LAYOUT) || |
| tag.equals(TtmlUtils.TAG_REGION) || tag.equals(TtmlUtils.TAG_METADATA) || |
| tag.equals(TtmlUtils.TAG_SMPTE_IMAGE) || tag.equals(TtmlUtils.TAG_SMPTE_DATA) || |
| tag.equals(TtmlUtils.TAG_SMPTE_INFORMATION)) { |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /** @hide */ |
| interface TtmlNodeListener { |
| void onTtmlNodeParsed(TtmlNode node); |
| void onRootNodeParsed(TtmlNode node); |
| } |
| |
| /** @hide */ |
| class TtmlTrack extends SubtitleTrack implements TtmlNodeListener { |
| private static final String TAG = "TtmlTrack"; |
| |
| private final TtmlParser mParser = new TtmlParser(this); |
| private final TtmlRenderingWidget mRenderingWidget; |
| private String mParsingData; |
| private Long mCurrentRunID; |
| |
| private final LinkedList<TtmlNode> mTtmlNodes; |
| private final TreeSet<Long> mTimeEvents; |
| private TtmlNode mRootNode; |
| |
| TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format) { |
| super(format); |
| |
| mTtmlNodes = new LinkedList<TtmlNode>(); |
| mTimeEvents = new TreeSet<Long>(); |
| mRenderingWidget = renderingWidget; |
| mParsingData = ""; |
| } |
| |
| @Override |
| public TtmlRenderingWidget getRenderingWidget() { |
| return mRenderingWidget; |
| } |
| |
| @Override |
| public void onData(byte[] data, boolean eos, long runID) { |
| try { |
| // TODO: handle UTF-8 conversion properly |
| String str = new String(data, "UTF-8"); |
| |
| // implement intermixing restriction for TTML. |
| synchronized(mParser) { |
| if (mCurrentRunID != null && runID != mCurrentRunID) { |
| throw new IllegalStateException( |
| "Run #" + mCurrentRunID + |
| " in progress. Cannot process run #" + runID); |
| } |
| mCurrentRunID = runID; |
| mParsingData += str; |
| if (eos) { |
| try { |
| mParser.parse(mParsingData, mCurrentRunID); |
| } catch (XmlPullParserException e) { |
| e.printStackTrace(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| finishedRun(runID); |
| mParsingData = ""; |
| mCurrentRunID = null; |
| } |
| } |
| } catch (java.io.UnsupportedEncodingException e) { |
| Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e); |
| } |
| } |
| |
| @Override |
| public void onTtmlNodeParsed(TtmlNode node) { |
| mTtmlNodes.addLast(node); |
| addTimeEvents(node); |
| } |
| |
| @Override |
| public void onRootNodeParsed(TtmlNode node) { |
| mRootNode = node; |
| TtmlCue cue = null; |
| while ((cue = getNextResult()) != null) { |
| addCue(cue); |
| } |
| mRootNode = null; |
| mTtmlNodes.clear(); |
| mTimeEvents.clear(); |
| } |
| |
| @Override |
| public void updateView(Vector<SubtitleTrack.Cue> activeCues) { |
| if (!mVisible) { |
| // don't keep the state if we are not visible |
| return; |
| } |
| |
| if (DEBUG && mTimeProvider != null) { |
| try { |
| Log.d(TAG, "at " + |
| (mTimeProvider.getCurrentTimeUs(false, true) / 1000) + |
| " ms the active cues are:"); |
| } catch (IllegalStateException e) { |
| Log.d(TAG, "at (illegal state) the active cues are:"); |
| } |
| } |
| |
| mRenderingWidget.setActiveCues(activeCues); |
| } |
| |
| /** |
| * Returns a {@link TtmlCue} in the presentation time order. |
| * {@code null} is returned if there is no more timed text to show. |
| */ |
| public TtmlCue getNextResult() { |
| while (mTimeEvents.size() >= 2) { |
| long start = mTimeEvents.pollFirst(); |
| long end = mTimeEvents.first(); |
| List<TtmlNode> activeCues = getActiveNodes(start, end); |
| if (!activeCues.isEmpty()) { |
| return new TtmlCue(start, end, |
| TtmlUtils.applySpacePolicy(TtmlUtils.extractText( |
| mRootNode, start, end), false), |
| TtmlUtils.extractTtmlFragment(mRootNode, start, end)); |
| } |
| } |
| return null; |
| } |
| |
| private void addTimeEvents(TtmlNode node) { |
| mTimeEvents.add(node.mStartTimeMs); |
| mTimeEvents.add(node.mEndTimeMs); |
| for (int i = 0; i < node.mChildren.size(); ++i) { |
| addTimeEvents(node.mChildren.get(i)); |
| } |
| } |
| |
| private List<TtmlNode> getActiveNodes(long startTimeUs, long endTimeUs) { |
| List<TtmlNode> activeNodes = new ArrayList<TtmlNode>(); |
| for (int i = 0; i < mTtmlNodes.size(); ++i) { |
| TtmlNode node = mTtmlNodes.get(i); |
| if (node.isActive(startTimeUs, endTimeUs)) { |
| activeNodes.add(node); |
| } |
| } |
| return activeNodes; |
| } |
| } |
| |
| /** |
| * Widget capable of rendering TTML captions. |
| * |
| * @hide |
| */ |
| class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget { |
| |
| /** Callback for rendering changes. */ |
| private OnChangedListener mListener; |
| private final TextView mTextView; |
| |
| public TtmlRenderingWidget(Context context) { |
| this(context, null); |
| } |
| |
| public TtmlRenderingWidget(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| // Cannot render text over video when layer type is hardware. |
| setLayerType(View.LAYER_TYPE_SOFTWARE, null); |
| |
| CaptioningManager captionManager = (CaptioningManager) context.getSystemService( |
| Context.CAPTIONING_SERVICE); |
| mTextView = new TextView(context); |
| mTextView.setTextColor(captionManager.getUserStyle().foregroundColor); |
| addView(mTextView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); |
| mTextView.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); |
| } |
| |
| @Override |
| public void setOnChangedListener(OnChangedListener listener) { |
| mListener = listener; |
| } |
| |
| @Override |
| public void setSize(int width, int height) { |
| final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); |
| final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); |
| |
| measure(widthSpec, heightSpec); |
| layout(0, 0, width, height); |
| } |
| |
| @Override |
| public void setVisible(boolean visible) { |
| if (visible) { |
| setVisibility(View.VISIBLE); |
| } else { |
| setVisibility(View.GONE); |
| } |
| } |
| |
| @Override |
| public void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| } |
| |
| public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) { |
| final int count = activeCues.size(); |
| String subtitleText = ""; |
| for (int i = 0; i < count; i++) { |
| TtmlCue cue = (TtmlCue) activeCues.get(i); |
| subtitleText += cue.mText + "\n"; |
| } |
| mTextView.setText(subtitleText); |
| |
| if (mListener != null) { |
| mListener.onChanged(this); |
| } |
| } |
| } |