WebVTT styling.
- parse webvtt cue - remove all tags from string (supported or not) - apply spans for b, i and u - honor class names in tags to properly parse the cue but do not apply styles for them
This commit is contained in:
parent
cdb6ac4073
commit
1a9b2be551
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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 com.google.android.exoplayer.text.webvtt;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
|
||||
/**
|
||||
* Unit test for {@link WebvttCueParser}.
|
||||
*/
|
||||
public final class WebvttCueParserTest extends InstrumentationTestCase {
|
||||
|
||||
public void testParseStrictValidClassesAndTrailingTokens() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("<v.first.loud Esme>"
|
||||
+ "This <u.style1.style2 some stuff>is</u> text with <b.foo><i.bar>html</i></b> tags");
|
||||
|
||||
assertEquals("This is text with html tags", text.toString());
|
||||
|
||||
UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);
|
||||
StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(1, underlineSpans.length);
|
||||
assertEquals(2, styleSpans.length);
|
||||
assertEquals(Typeface.ITALIC, styleSpans[0].getStyle());
|
||||
assertEquals(Typeface.BOLD, styleSpans[1].getStyle());
|
||||
|
||||
assertEquals(5, text.getSpanStart(underlineSpans[0]));
|
||||
assertEquals(7, text.getSpanEnd(underlineSpans[0]));
|
||||
assertEquals(18, text.getSpanStart(styleSpans[0]));
|
||||
assertEquals(18, text.getSpanStart(styleSpans[1]));
|
||||
assertEquals(22, text.getSpanEnd(styleSpans[0]));
|
||||
assertEquals(22, text.getSpanEnd(styleSpans[1]));
|
||||
}
|
||||
|
||||
public void testParseStrictValidUnsupportedTagsStrippedOut() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse(
|
||||
"<v.first.loud Esme>This <unsupported>is</unsupported> text with "
|
||||
+ "<notsupp><invalid>html</invalid></notsupp> tags");
|
||||
|
||||
assertEquals("This is text with html tags", text.toString());
|
||||
assertEquals(0, getSpans(text, UnderlineSpan.class).length);
|
||||
assertEquals(0, getSpans(text, StyleSpan.class).length);
|
||||
}
|
||||
|
||||
public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse(
|
||||
"An <u some trailing stuff>unclosed u tag with <i>italic</i> inside");
|
||||
|
||||
assertEquals("An unclosed u tag with italic inside", text.toString());
|
||||
|
||||
UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);
|
||||
StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(1, underlineSpans.length);
|
||||
assertEquals(1, styleSpans.length);
|
||||
assertEquals(Typeface.ITALIC, styleSpans[0].getStyle());
|
||||
|
||||
assertEquals(3, text.getSpanStart(underlineSpans[0]));
|
||||
assertEquals(23, text.getSpanStart(styleSpans[0]));
|
||||
assertEquals(29, text.getSpanEnd(styleSpans[0]));
|
||||
assertEquals(36, text.getSpanEnd(underlineSpans[0]));
|
||||
}
|
||||
|
||||
public void testParseWellFormedUnclosedEndAtParent() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse(
|
||||
"An unclosed u tag with <i><u>underline and italic</i> inside");
|
||||
|
||||
assertEquals("An unclosed u tag with underline and italic inside", text.toString());
|
||||
|
||||
UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);
|
||||
StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(1, underlineSpans.length);
|
||||
assertEquals(1, styleSpans.length);
|
||||
|
||||
assertEquals(23, text.getSpanStart(underlineSpans[0]));
|
||||
assertEquals(23, text.getSpanStart(styleSpans[0]));
|
||||
assertEquals(43, text.getSpanEnd(underlineSpans[0]));
|
||||
assertEquals(43, text.getSpanEnd(styleSpans[0]));
|
||||
|
||||
assertEquals(Typeface.ITALIC, styleSpans[0].getStyle());
|
||||
}
|
||||
|
||||
public void testParseMalformedNestedElements() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse(
|
||||
"<b><u>An unclosed u tag with <i>italic</u> inside</i></b>");
|
||||
assertEquals("An unclosed u tag with italic inside", text.toString());
|
||||
|
||||
UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);
|
||||
StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(1, underlineSpans.length);
|
||||
assertEquals(2, styleSpans.length);
|
||||
|
||||
// all tags applied until matching start tag found
|
||||
assertEquals(0, text.getSpanStart(underlineSpans[0]));
|
||||
assertEquals(29, text.getSpanEnd(underlineSpans[0]));
|
||||
if (styleSpans[0].getStyle() == Typeface.BOLD) {
|
||||
assertEquals(0, text.getSpanStart(styleSpans[0]));
|
||||
assertEquals(23, text.getSpanStart(styleSpans[1]));
|
||||
assertEquals(29, text.getSpanEnd(styleSpans[1]));
|
||||
assertEquals(36, text.getSpanEnd(styleSpans[0]));
|
||||
} else {
|
||||
assertEquals(0, text.getSpanStart(styleSpans[1]));
|
||||
assertEquals(23, text.getSpanStart(styleSpans[0]));
|
||||
assertEquals(29, text.getSpanEnd(styleSpans[0]));
|
||||
assertEquals(36, text.getSpanEnd(styleSpans[1]));
|
||||
}
|
||||
}
|
||||
|
||||
public void testParseCloseNonExistingTag() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("blah<b>blah</i>blah</b>blah");
|
||||
assertEquals("blahblahblahblah", text.toString());
|
||||
|
||||
StyleSpan[] spans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(1, spans.length);
|
||||
assertEquals(Typeface.BOLD, spans[0].getStyle());
|
||||
assertEquals(4, text.getSpanStart(spans[0]));
|
||||
assertEquals(8, text.getSpanEnd(spans[0])); // should be 12 when valid
|
||||
}
|
||||
|
||||
public void testParseEmptyTagName() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("An unclosed u tag with <>italic inside");
|
||||
assertEquals("An unclosed u tag with italic inside", text.toString());
|
||||
}
|
||||
|
||||
public void testParseEntities() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("& > < ");
|
||||
assertEquals("& > < ", text.toString());
|
||||
}
|
||||
|
||||
public void testParseEntitiesUnsupported() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("&noway; &sure;");
|
||||
assertEquals(" ", text.toString());
|
||||
}
|
||||
|
||||
public void testParseEntitiesNotTerminated() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("& here comes text");
|
||||
assertEquals("& here comes text", text.toString());
|
||||
}
|
||||
|
||||
public void testParseEntitiesNotTerminatedUnsupported() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("&surenot here comes text");
|
||||
assertEquals(" here comes text", text.toString());
|
||||
}
|
||||
|
||||
public void testParseEntitiesNotTerminatedNoSpace() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("&surenot");
|
||||
assertEquals("&surenot", text.toString());
|
||||
}
|
||||
|
||||
public void testParseVoidTag() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("here comes<br/> text<br/>");
|
||||
assertEquals("here comes text", text.toString());
|
||||
}
|
||||
|
||||
public void testParseMultipleTagsOfSameKind() {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("blah <b>blah</b> blah <b>foo</b>");
|
||||
|
||||
assertEquals("blah blah blah foo", text.toString());
|
||||
StyleSpan[] spans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(2, spans.length);
|
||||
assertEquals(5, text.getSpanStart(spans[0]));
|
||||
assertEquals(9, text.getSpanEnd(spans[0]));
|
||||
assertEquals(15, text.getSpanStart(spans[1]));
|
||||
assertEquals(18, text.getSpanEnd(spans[1]));
|
||||
assertEquals(Typeface.BOLD, spans[0].getStyle());
|
||||
assertEquals(Typeface.BOLD, spans[1].getStyle());
|
||||
}
|
||||
|
||||
public void testParseInvalidVoidSlash() {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse("blah <b/.st1.st2 trailing stuff> blah");
|
||||
|
||||
assertEquals("blah blah", text.toString());
|
||||
StyleSpan[] spans = getSpans(text, StyleSpan.class);
|
||||
assertEquals(0, spans.length);
|
||||
}
|
||||
|
||||
public void testParseMonkey() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse(
|
||||
"< u>An unclosed u tag with <<<<< i>italic</u></u></u></u ></i><u><u> inside");
|
||||
assertEquals("An unclosed u tag with italic inside", text.toString());
|
||||
text = parser.parse(">>>>>>>>>An unclosed u tag with <<<<< italic</u></u></u></u >"
|
||||
+ "</i><u><u> inside");
|
||||
assertEquals(">>>>>>>>>An unclosed u tag with inside", text.toString());
|
||||
}
|
||||
|
||||
public void testParseCornerCases() throws Exception {
|
||||
WebvttCueParser parser = new WebvttCueParser();
|
||||
Spanned text = parser.parse(">");
|
||||
assertEquals(">", text.toString());
|
||||
|
||||
text = parser.parse("<");
|
||||
assertEquals("", text.toString());
|
||||
|
||||
text = parser.parse("<b.st1.st2 annotation");
|
||||
assertEquals("", text.toString());
|
||||
|
||||
text = parser.parse("<<<<<<<<<<<<<<<<");
|
||||
assertEquals("", text.toString());
|
||||
|
||||
text = parser.parse("<<<<<<>><<<<<<<<<<");
|
||||
assertEquals(">", text.toString());
|
||||
|
||||
text = parser.parse("<>");
|
||||
assertEquals("", text.toString());
|
||||
|
||||
text = parser.parse("&");
|
||||
assertEquals("&", text.toString());
|
||||
|
||||
text = parser.parse("&&&&&&&");
|
||||
assertEquals("&&&&&&&", text.toString());
|
||||
}
|
||||
|
||||
private static <T> T[] getSpans(Spanned text, Class<T> spanType) {
|
||||
return text.getSpans(0, text.length(), spanType);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 com.google.android.exoplayer.text.webvtt;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Parser for webvtt cue text. (https://w3c.github.io/webvtt/#cue-text)
|
||||
*/
|
||||
/* package */ final class WebvttCueParser {
|
||||
|
||||
private static final char CHAR_LESS_THAN = '<';
|
||||
private static final char CHAR_GREATER_THAN = '>';
|
||||
private static final char CHAR_SLASH = '/';
|
||||
private static final char CHAR_AMPERSAND = '&';
|
||||
private static final char CHAR_SEMI_COLON = ';';
|
||||
private static final char CHAR_SPACE = ' ';
|
||||
private static final String SPACE = " ";
|
||||
|
||||
private static final String ENTITY_LESS_THAN = "lt";
|
||||
private static final String ENTITY_GREATER_THAN = "gt";
|
||||
private static final String ENTITY_AMPERSAND = "amp";
|
||||
private static final String ENTITY_NON_BREAK_SPACE = "nbsp";
|
||||
|
||||
private static final String TAG_BOLD = "b";
|
||||
private static final String TAG_ITALIC = "i";
|
||||
private static final String TAG_UNDERLINE = "u";
|
||||
private static final String TAG_CLASS = "c";
|
||||
private static final String TAG_VOICE = "v";
|
||||
private static final String TAG_LANG = "lang";
|
||||
|
||||
private static final int STYLE_BOLD = Typeface.BOLD;
|
||||
private static final int STYLE_ITALIC = Typeface.ITALIC;
|
||||
|
||||
private static final String TAG = "WebvttCueParser";
|
||||
|
||||
public Spanned parse(String markup) {
|
||||
SpannableStringBuilder spannedText = new SpannableStringBuilder();
|
||||
Stack<StartTag> startTagStack = new Stack<>();
|
||||
String[] tagTokens;
|
||||
int pos = 0;
|
||||
while (pos < markup.length()) {
|
||||
char curr = markup.charAt(pos);
|
||||
switch (curr) {
|
||||
case CHAR_LESS_THAN:
|
||||
if (pos + 1 >= markup.length()) {
|
||||
pos++;
|
||||
break; // avoid ArrayOutOfBoundsException
|
||||
}
|
||||
int ltPos = pos;
|
||||
boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH;
|
||||
pos = findEndOfTag(markup, ltPos + 1);
|
||||
boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH;
|
||||
|
||||
tagTokens = tokenizeTag(markup.substring(
|
||||
ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1));
|
||||
if (tagTokens == null || !isSupportedTag(tagTokens[0])) {
|
||||
continue;
|
||||
}
|
||||
if (isClosingTag) {
|
||||
StartTag startTag;
|
||||
do {
|
||||
if (startTagStack.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
startTag = startTagStack.pop();
|
||||
applySpansForTag(startTag, spannedText);
|
||||
} while(!startTag.name.equals(tagTokens[0]));
|
||||
} else if (!isVoidTag) {
|
||||
startTagStack.push(new StartTag(tagTokens[0], spannedText.length()));
|
||||
}
|
||||
break;
|
||||
case CHAR_AMPERSAND:
|
||||
int semiColonEnd = markup.indexOf(CHAR_SEMI_COLON, pos + 1);
|
||||
int spaceEnd = markup.indexOf(CHAR_SPACE, pos + 1);
|
||||
int entityEnd = semiColonEnd == -1 ? spaceEnd
|
||||
: spaceEnd == -1 ? semiColonEnd : Math.min(semiColonEnd, spaceEnd);
|
||||
if (entityEnd != -1) {
|
||||
applyEntity(markup.substring(pos + 1, entityEnd), spannedText);
|
||||
if (entityEnd == spaceEnd) {
|
||||
spannedText.append(" ");
|
||||
}
|
||||
pos = entityEnd + 1;
|
||||
} else {
|
||||
spannedText.append(curr);
|
||||
pos++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
spannedText.append(curr);
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// apply unclosed tags
|
||||
while (!startTagStack.isEmpty()) {
|
||||
applySpansForTag(startTagStack.pop(), spannedText);
|
||||
}
|
||||
return spannedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find end of tag (>). The position returned is the position of the > plus one (exclusive).
|
||||
*
|
||||
* @param markup The webvtt cue markup to be parsed.
|
||||
* @param startPos the position from where to start searching for the end of tag.
|
||||
* @return the position of the end of tag plus 1 (one).
|
||||
*/
|
||||
private int findEndOfTag(String markup, int startPos) {
|
||||
int idx = markup.indexOf(CHAR_GREATER_THAN, startPos);
|
||||
return idx == -1 ? markup.length() : idx + 1;
|
||||
}
|
||||
|
||||
private void applyEntity(String entity, SpannableStringBuilder spannedText) {
|
||||
switch (entity) {
|
||||
case ENTITY_LESS_THAN:
|
||||
spannedText.append('<');
|
||||
break;
|
||||
case ENTITY_GREATER_THAN:
|
||||
spannedText.append('>');
|
||||
break;
|
||||
case ENTITY_NON_BREAK_SPACE:
|
||||
spannedText.append(' ');
|
||||
break;
|
||||
case ENTITY_AMPERSAND:
|
||||
spannedText.append('&');
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "ignoring unsupported entity: '&" + entity + ";'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSupportedTag(String tagName) {
|
||||
switch (tagName) {
|
||||
case TAG_BOLD:
|
||||
case TAG_CLASS:
|
||||
case TAG_ITALIC:
|
||||
case TAG_LANG:
|
||||
case TAG_UNDERLINE:
|
||||
case TAG_VOICE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void applySpansForTag(StartTag startTag, SpannableStringBuilder spannedText) {
|
||||
switch(startTag.name) {
|
||||
case TAG_BOLD:
|
||||
spannedText.setSpan(new StyleSpan(STYLE_BOLD), startTag.position,
|
||||
spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return;
|
||||
case TAG_ITALIC:
|
||||
spannedText.setSpan(new StyleSpan(STYLE_ITALIC), startTag.position,
|
||||
spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return;
|
||||
case TAG_UNDERLINE:
|
||||
spannedText.setSpan(new UnderlineSpan(), startTag.position,
|
||||
spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes a tag expression into tag name (pos 0) and classes (pos 1..n).
|
||||
*
|
||||
* @param fullTagExpression characters between &lt: and &gt; of a start or end tag
|
||||
* @return an array of <code>String</code>s with the tag name at pos 0 followed by style classes
|
||||
* or null if it's an empty tag: '<>'
|
||||
*/
|
||||
private String[] tokenizeTag(String fullTagExpression) {
|
||||
fullTagExpression = fullTagExpression.replace("\\s+", " ").trim();
|
||||
if (fullTagExpression.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
if (fullTagExpression.contains(SPACE)) {
|
||||
fullTagExpression = fullTagExpression.substring(0, fullTagExpression.indexOf(SPACE));
|
||||
}
|
||||
return fullTagExpression.split("\\.");
|
||||
}
|
||||
|
||||
private static final class StartTag {
|
||||
|
||||
public final String name;
|
||||
public final int position;
|
||||
|
||||
public StartTag(String name, int position) {
|
||||
this.position = position;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -21,7 +21,6 @@ import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.text.SubtitleParser;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.text.Html;
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@ -48,10 +47,12 @@ public final class WebvttParser implements SubtitleParser {
|
||||
private static final Pattern CUE_HEADER = Pattern.compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
|
||||
private static final Pattern CUE_SETTING = Pattern.compile("(\\S+?):(\\S+)");
|
||||
|
||||
private final WebvttCueParser cueParser;
|
||||
private final PositionHolder positionHolder;
|
||||
private final StringBuilder textBuilder;
|
||||
|
||||
public WebvttParser() {
|
||||
this.cueParser = new WebvttCueParser();
|
||||
positionHolder = new PositionHolder();
|
||||
textBuilder = new StringBuilder();
|
||||
}
|
||||
@ -130,11 +131,12 @@ public final class WebvttParser implements SubtitleParser {
|
||||
String line;
|
||||
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
|
||||
if (textBuilder.length() > 0) {
|
||||
textBuilder.append("<br>");
|
||||
textBuilder.append("\n");
|
||||
}
|
||||
textBuilder.append(line.trim());
|
||||
}
|
||||
CharSequence cueText = Html.fromHtml(textBuilder.toString());
|
||||
|
||||
CharSequence cueText = cueParser.parse(textBuilder.toString());
|
||||
|
||||
WebvttCue cue = new WebvttCue(cueStartTime, cueEndTime, cueText, cueTextAlignment, cueLine,
|
||||
cueLineType, cueLineAnchor, cuePosition, cuePositionAnchor, cueWidth);
|
||||
|
Loading…
x
Reference in New Issue
Block a user