750 lines
27 KiB
Java
750 lines
27 KiB
Java
/*
|
|
* 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.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
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 org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
|
|
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;
|
|
|
|
/** @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;
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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);
|
|
}
|
|
}
|
|
}
|