diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java
index 9e3ae65eee..13a14d0033 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java
@@ -32,4 +32,12 @@ import com.google.android.exoplayer2.util.Util;
"rgba(%d,%d,%d,%.3f)",
Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color) / 255.0);
}
+
+ /**
+ * Returns a CSS selector that selects all elements with {@code class=className} and all their
+ * descendants.
+ */
+ public static String cssAllClassDescendantsSelector(String className) {
+ return "." + className + ",." + className + " *";
+ }
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java
index 4faab3107b..7ea2b55cf4 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java
@@ -33,10 +33,15 @@ import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSp
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.regex.Pattern;
/**
@@ -46,7 +51,6 @@ import java.util.regex.Pattern;
*
Supports all of the spans used by ExoPlayer's subtitle decoders, including custom ones found
* in {@link com.google.android.exoplayer2.text.span}.
*/
-// TODO: Add support for more span types - only a small selection are currently implemented.
/* package */ final class SpannedToHtmlConverter {
// Matches /n and /r/n in ampersand-encoding (returned from Html.escapeHtml).
@@ -74,16 +78,29 @@ import java.util.regex.Pattern;
* @param displayDensity The screen density of the device. WebView treats 1 CSS px as one Android
* dp, so to convert size values from Android px to CSS px we need to know the screen density.
*/
- public static String convert(@Nullable CharSequence text, float displayDensity) {
+ public static HtmlAndCss convert(@Nullable CharSequence text, float displayDensity) {
if (text == null) {
- return "";
+ return new HtmlAndCss("", /* cssRuleSets= */ ImmutableMap.of());
}
if (!(text instanceof Spanned)) {
- return escapeHtml(text);
+ return new HtmlAndCss(escapeHtml(text), /* cssRuleSets= */ ImmutableMap.of());
}
Spanned spanned = (Spanned) text;
- SparseArray spanTransitions = findSpanTransitions(spanned, displayDensity);
+ // Use CSS inheritance to ensure BackgroundColorSpans affect all inner elements
+ Set backgroundColors = new HashSet<>();
+ for (BackgroundColorSpan backgroundColorSpan :
+ spanned.getSpans(0, spanned.length(), BackgroundColorSpan.class)) {
+ backgroundColors.add(backgroundColorSpan.getBackgroundColor());
+ }
+ HashMap cssRuleSets = new HashMap<>();
+ for (int backgroundColor : backgroundColors) {
+ cssRuleSets.put(
+ HtmlUtils.cssAllClassDescendantsSelector("bg_" + backgroundColor),
+ Util.formatInvariant("background-color:%s;", HtmlUtils.toCssRgba(backgroundColor)));
+ }
+
+ SparseArray spanTransitions = findSpanTransitions(spanned, displayDensity);
StringBuilder html = new StringBuilder(spanned.length());
int previousTransition = 0;
for (int i = 0; i < spanTransitions.size(); i++) {
@@ -104,7 +121,7 @@ import java.util.regex.Pattern;
html.append(escapeHtml(spanned.subSequence(previousTransition, spanned.length())));
- return html.toString();
+ return new HtmlAndCss(html.toString(), cssRuleSets);
}
private static SparseArray findSpanTransitions(
@@ -137,9 +154,7 @@ import java.util.regex.Pattern;
"", HtmlUtils.toCssRgba(colorSpan.getForegroundColor()));
} else if (span instanceof BackgroundColorSpan) {
BackgroundColorSpan colorSpan = (BackgroundColorSpan) span;
- return Util.formatInvariant(
- "",
- HtmlUtils.toCssRgba(colorSpan.getBackgroundColor()));
+ return Util.formatInvariant("", colorSpan.getBackgroundColor());
} else if (span instanceof HorizontalTextInVerticalContextSpan) {
return "";
} else if (span instanceof AbsoluteSizeSpan) {
@@ -231,6 +246,26 @@ import java.util.regex.Pattern;
return NEWLINE_PATTERN.matcher(escaped).replaceAll("
");
}
+ /** Container class for an HTML string and associated CSS rulesets. */
+ public static class HtmlAndCss {
+
+ /** A raw HTML string. */
+ public final String html;
+
+ /**
+ * CSS rulesets used to style {@link #html}.
+ *
+ * Each key is a CSS selector, and each value is a CSS declaration (i.e. a semi-colon
+ * separated list of colon-separated key-value pairs, e.g "prop1:val1;prop2:val2;").
+ */
+ public final Map cssRuleSets;
+
+ private HtmlAndCss(String html, Map cssRuleSets) {
+ this.html = html;
+ this.cssRuleSets = cssRuleSets;
+ }
+ }
+
private static final class SpanInfo {
/**
* Sort by end index (descending), then by opening tag and then closing tag (both ascending, for
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java
index 835a4df43a..f3de4298a5 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java
@@ -31,11 +31,14 @@ import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* A {@link SubtitleView.Output} that uses a {@link WebView} to render subtitles.
@@ -51,6 +54,8 @@ import java.util.List;
*/
private static final float CSS_LINE_HEIGHT = 1.2f;
+ private static final String DEFAULT_BACKGROUND_CSS_CLASS = "default_bg";
+
/**
* A {@link CanvasSubtitleOutput} used for displaying bitmap cues.
*
@@ -162,7 +167,7 @@ import java.util.List;
StringBuilder html = new StringBuilder();
html.append(
Util.formatInvariant(
- " cssRuleSets = new HashMap<>();
+ cssRuleSets.put(
+ HtmlUtils.cssAllClassDescendantsSelector(DEFAULT_BACKGROUND_CSS_CLASS),
+ Util.formatInvariant("background-color:%s;", HtmlUtils.toCssRgba(style.backgroundColor)));
for (int i = 0; i < textCues.size(); i++) {
Cue cue = textCues.get(i);
float positionPercent = (cue.position != Cue.DIMEN_UNSET) ? (cue.position * 100) : 50;
@@ -255,6 +262,18 @@ import java.util.List;
verticalTranslatePercent = lineAnchorTranslatePercent;
}
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(
+ cue.text, getContext().getResources().getDisplayMetrics().density);
+ for (String cssSelector : cssRuleSets.keySet()) {
+ @Nullable
+ String previousCssDeclarationBlock =
+ cssRuleSets.put(cssSelector, cssRuleSets.get(cssSelector));
+ Assertions.checkState(
+ previousCssDeclarationBlock == null
+ || previousCssDeclarationBlock.equals(cssRuleSets.get(cssSelector)));
+ }
+
html.append(
Util.formatInvariant(
"
", backgroundColorCss))
- .append(
- SpannedToHtmlConverter.convert(
- cue.text, getContext().getResources().getDisplayMetrics().density))
+ .append(Util.formatInvariant("", DEFAULT_BACKGROUND_CSS_CLASS))
+ .append(htmlAndCss.html)
.append("")
.append("
");
}
-
html.append("
");
+ StringBuilder htmlHead = new StringBuilder();
+ htmlHead.append("");
+ html.insert(0, htmlHead.toString());
+
webView.loadData(
Base64.encodeToString(html.toString().getBytes(Charsets.UTF_8), Base64.NO_PADDING),
"text/html",
diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java
index f595d4233b..b9eb6d8e6a 100644
--- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java
+++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java
@@ -58,27 +58,33 @@ public class SpannedToHtmlConverterTest {
"String with colored".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo("String with colored section");
}
@Test
public void convert_supportsBackgroundColorSpan() {
SpannableString spanned = new SpannableString("String with highlighted section");
+ int color = Color.argb(51, 64, 32, 16);
spanned.setSpan(
- new BackgroundColorSpan(Color.argb(51, 64, 32, 16)),
+ new BackgroundColorSpan(color),
"String with ".length(),
"String with highlighted".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
- .isEqualTo(
- "String with highlighted"
- + " section");
+ // Double check the color int is being used for the class name as we expect.
+ assertThat(color).isEqualTo(859840528);
+ assertThat(htmlAndCss.cssRuleSets)
+ .containsExactly(".bg_859840528,.bg_859840528 *", "background-color:rgba(64,32,16,0.200);");
+ assertThat(htmlAndCss.html)
+ .isEqualTo("String with highlighted" + " section");
}
@Test
@@ -90,9 +96,11 @@ public class SpannedToHtmlConverterTest {
"Vertical text with 123".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo(
"Vertical text with 123 "
+ "horizontal numbers");
@@ -109,11 +117,14 @@ public class SpannedToHtmlConverterTest {
"String with 10px".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
// 10 Android px are converted to 5 CSS px because WebView treats 1 CSS px as 1 Android dp
// and we're using screen density xhdpi i.e. density=2.
- assertThat(html).isEqualTo("String with 10px section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
+ .isEqualTo("String with 10px section");
}
// Set the screen density so we see that px are handled differently to dp.
@@ -127,9 +138,12 @@ public class SpannedToHtmlConverterTest {
"String with 10dp".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with 10dp section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
+ .isEqualTo("String with 10dp section");
}
@Test
@@ -141,9 +155,12 @@ public class SpannedToHtmlConverterTest {
"String with 10%".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with 10% section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
+ .isEqualTo("String with 10% section");
}
@Test
@@ -155,9 +172,11 @@ public class SpannedToHtmlConverterTest {
"String with Times New Roman".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo(
"String with Times New Roman"
+ " section");
@@ -172,9 +191,11 @@ public class SpannedToHtmlConverterTest {
"String with unstyled".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with unstyled section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with unstyled section");
}
@Test
@@ -186,9 +207,11 @@ public class SpannedToHtmlConverterTest {
"String with crossed-out".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo(
"String with crossed-out section");
}
@@ -213,9 +236,11 @@ public class SpannedToHtmlConverterTest {
"String with bold, italic and bold-italic".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo(
"String with bold, italic and bold-italic sections.");
}
@@ -235,9 +260,11 @@ public class SpannedToHtmlConverterTest {
"String with over-annotated and under-annotated".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo(
"String with "
+ ""
@@ -261,33 +288,39 @@ public class SpannedToHtmlConverterTest {
"String with underlined".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with underlined section.");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with underlined section.");
}
@Test
public void convert_escapesHtmlInUnspannedString() {
- String html = SpannedToHtmlConverter.convert("String with bold tags", displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert("String with bold tags", displayDensity);
- assertThat(html).isEqualTo("String with <b>bold</b> tags");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with <b>bold</b> tags");
}
@Test
public void convert_handlesLinebreakInUnspannedString() {
- String html =
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert(
"String with\nnew line and\r\ncrlf style too", displayDensity);
- assertThat(html).isEqualTo("String with
new line and
crlf style too");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with
new line and
crlf style too");
}
@Test
public void convert_doesntConvertAmpersandLineFeedToBrTag() {
- String html =
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert("String with
new line ampersand code", displayDensity);
- assertThat(html).isEqualTo("String with new line ampersand code");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with new line ampersand code");
}
@Test
@@ -299,27 +332,32 @@ public class SpannedToHtmlConverterTest {
"String with unrecognised".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with <foo>unrecognised</foo> tags");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
+ .isEqualTo("String with <foo>unrecognised</foo> tags");
}
@Test
public void convert_handlesLinebreakInSpannedString() {
- String html =
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert(
"String with\nnew line and\r\ncrlf style too", displayDensity);
- assertThat(html).isEqualTo("String with
new line and
crlf style too");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with
new line and
crlf style too");
}
@Test
public void convert_convertsNonAsciiCharactersToAmpersandCodes() {
- String html =
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert(
new SpannableString("Strìng with 優しいの non-ASCII characters"), displayDensity);
- assertThat(html)
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
.isEqualTo("Strìng with 優しいの non-ASCII characters");
}
@@ -337,9 +375,11 @@ public class SpannedToHtmlConverterTest {
"String with unrecognised".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with unrecognised span");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with unrecognised span");
}
@Test
@@ -351,9 +391,12 @@ public class SpannedToHtmlConverterTest {
spanned.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with italic-bold-underlined section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html)
+ .isEqualTo("String with italic-bold-underlined section");
}
@Test
@@ -368,9 +411,11 @@ public class SpannedToHtmlConverterTest {
"String with italic and bold".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with italic and bold section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with italic and bold section");
}
@Test
@@ -387,8 +432,10 @@ public class SpannedToHtmlConverterTest {
"String with italic and bold section".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- String html = SpannedToHtmlConverter.convert(spanned, displayDensity);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
+ SpannedToHtmlConverter.convert(spanned, displayDensity);
- assertThat(html).isEqualTo("String with italic and bold section");
+ assertThat(htmlAndCss.cssRuleSets).isEmpty();
+ assertThat(htmlAndCss.html).isEqualTo("String with italic and bold section");
}
}