/* * 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. *
* For the format of the time expression, please refer timeExpression
*
* @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 the
* default space policy 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 the
* default space policy 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("
* Supported features in this parser are:
*
");
} 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
*
*