diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index b515eca98a..57b063dbb2 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -506,6 +506,13 @@ "subtitle_mime_type": "text/x-ssa", "subtitle_language": "en" }, + { + "name": "SubStation Alpha colors", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", + "subtitle_uri": "https://drive.google.com/uc?export=download&id=13EdW4Qru-vQerUlwS_Ht5Cely_Tn0tQe", + "subtitle_mime_type": "text/x-ssa", + "subtitle_language": "en" + }, { "name": "MPEG-4 Timed Text", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4" diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index f44db4924f..fa66c49dfe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -19,6 +19,8 @@ import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_FRACTION; import static com.google.android.exoplayer2.util.Util.castNonNull; import android.text.Layout; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -301,8 +303,17 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { SsaStyle.Overrides styleOverrides, float screenWidth, float screenHeight) { - Cue.Builder cue = new Cue.Builder().setText(text); + SpannableString spannableText = new SpannableString(text); + Cue.Builder cue = new Cue.Builder().setText(spannableText); + // Apply primary color. + if (style != null) { + if (style.primaryColor != SsaStyle.SSA_COLOR_UNKNOWN) { + spannableText.setSpan(new ForegroundColorSpan(style.primaryColor), + 0, spannableText.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + // Apply alignment. @SsaStyle.SsaAlignment int alignment; if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { alignment = styleOverrides.alignment; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index 0cba339034..f2c0eb630c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -21,10 +21,12 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.graphics.PointF; import android.text.TextUtils; +import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -83,12 +85,16 @@ import java.util.regex.Pattern; public static final int SSA_ALIGNMENT_TOP_CENTER = 8; public static final int SSA_ALIGNMENT_TOP_RIGHT = 9; + public static final int SSA_COLOR_UNKNOWN = -1; + public final String name; @SsaAlignment public final int alignment; + @ColorInt public int primaryColor; - private SsaStyle(String name, @SsaAlignment int alignment) { + private SsaStyle(String name, @SsaAlignment int alignment, @ColorInt int primaryColor) { this.name = name; this.alignment = alignment; + this.primaryColor = primaryColor; } @Nullable @@ -105,7 +111,9 @@ import java.util.regex.Pattern; } try { return new SsaStyle( - styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); + styleValues[format.nameIndex].trim(), + parseAlignment(styleValues[format.alignmentIndex]), + parsePrimaryColor(styleValues[format.primaryColorIndex])); } catch (RuntimeException e) { Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); return null; @@ -144,6 +152,16 @@ import java.util.regex.Pattern; } } + @ColorInt + private static int parsePrimaryColor(String primaryColorStr) { + try { + return ColorParser.parseSsaColor(primaryColorStr); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "Failed parsing color value: " + primaryColorStr); + } + return SSA_COLOR_UNKNOWN; + } + /** * Represents a {@code Format:} line from the {@code [V4+ Styles]} section * @@ -154,11 +172,13 @@ import java.util.regex.Pattern; public final int nameIndex; public final int alignmentIndex; + public final int primaryColorIndex; public final int length; - private Format(int nameIndex, int alignmentIndex, int length) { + private Format(int nameIndex, int alignmentIndex, int primaryColorIndex, int length) { this.nameIndex = nameIndex; this.alignmentIndex = alignmentIndex; + this.primaryColorIndex = primaryColorIndex; this.length = length; } @@ -171,6 +191,7 @@ import java.util.regex.Pattern; public static Format fromFormatLine(String styleFormatLine) { int nameIndex = C.INDEX_UNSET; int alignmentIndex = C.INDEX_UNSET; + int primaryColorIndex = C.INDEX_UNSET; String[] keys = TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); for (int i = 0; i < keys.length; i++) { @@ -181,9 +202,14 @@ import java.util.regex.Pattern; case "alignment": alignmentIndex = i; break; + case "primarycolour": + primaryColorIndex = i; + break; } } - return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null; + return nameIndex != C.INDEX_UNSET + ? new Format(nameIndex, alignmentIndex, primaryColorIndex, keys.length) + : null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java index 85ef43f669..697c1695e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java @@ -67,6 +67,27 @@ public final class ColorParser { return parseColorInternal(colorExpression, true); } + /** + * Parses a SSA V4+ color expression. + * + * @param colorExpression The color expression. + * @return The parsed ARGB color. + */ + @ColorInt + public static int parseSsaColor(String colorExpression) { + // SSA V4+ color format is &HAABBGGRR. + if (colorExpression.length() != 10 || !"&H".equals(colorExpression.substring(0, 2))) { + throw new IllegalArgumentException(); + } + // Convert &HAABBGGRR to #RRGGBBAA. + String rgba = new StringBuilder() + .append(colorExpression.substring(2)) + .append("#") + .reverse() + .toString(); + return parseColorInternal(rgba, true); + } + @ColorInt private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) { Assertions.checkArgument(!TextUtils.isEmpty(colorExpression)); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java index c2f165dec1..a15ef95627 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -18,8 +18,10 @@ package com.google.android.exoplayer2.util; import static android.graphics.Color.BLACK; import static android.graphics.Color.RED; import static android.graphics.Color.WHITE; +import static android.graphics.Color.YELLOW; import static android.graphics.Color.argb; import static android.graphics.Color.parseColor; +import static com.google.android.exoplayer2.util.ColorParser.parseSsaColor; import static com.google.android.exoplayer2.util.ColorParser.parseTtmlColor; import static com.google.common.truth.Truth.assertThat; @@ -64,6 +66,9 @@ public final class ColorParserTest { // Hex colors in ColorParser are RGBA, where-as {@link Color#parseColor} takes ARGB. assertThat(parseTtmlColor("#FFFFFF00")).isEqualTo(parseColor("#00FFFFFF")); assertThat(parseTtmlColor("#12345678")).isEqualTo(parseColor("#78123456")); + // SSA colors are in &HAABBGGRR format. + assertThat(parseSsaColor("&HFF0000FF")).isEqualTo(RED); + assertThat(parseSsaColor("&HFF00FFFF")).isEqualTo(YELLOW); } @Test