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;
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);
}
}
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;
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() {
}
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);
}
public static String applyDefaultSpacePolicy(String in) {
return applySpacePolicy(in, true);
}
public static String applySpacePolicy(String in, boolean treatLfAsSpace) {
String crRemoved = in.replaceAll("\r\n", "\n");
String spacesNeighboringLfRemoved = crRemoved.replaceAll(" *\n *", "\n");
String lfToSpace = treatLfAsSpace ? spacesNeighboringLfRemoved.replaceAll("\n", " ")
: spacesNeighboringLfRemoved;
String spacesCollapsed = lfToSpace.replaceAll("[ \t\\x0B\f\r]+", " ");
return spacesCollapsed;
}
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)) {
} 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");
}
}
}
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(">");
}
}
}
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;
}
}
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;
}
public boolean isActive(long startTimeMs, long endTimeMs) {
return this.mEndTimeMs > startTimeMs && this.mStartTimeMs < endTimeMs;
}
}
class TtmlParser {
static final String TAG = "TtmlParser";
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;
}
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);
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 (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;
}
}
interface TtmlNodeListener {
void onTtmlNodeParsed(TtmlNode node);
void onRootNodeParsed(TtmlNode node);
}
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 {
String str = new String(data, "UTF-8");
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) {
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);
}
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;
}
}
class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget {
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);
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);
}
}
}