Merge pull request #8490 from szaboa:dev-2-8435-ssa-color

PiperOrigin-RevId: 354293679
This commit is contained in:
Oliver Woodman 2021-02-01 18:10:35 +00:00
commit c9fce083f3
5 changed files with 156 additions and 8 deletions

View File

@ -5,6 +5,9 @@
* Extractors: * Extractors:
* Fix Vorbis private codec data parsing in the Matroska extractor * Fix Vorbis private codec data parsing in the Matroska extractor
([#8496](https://github.com/google/ExoPlayer/issues/8496)). ([#8496](https://github.com/google/ExoPlayer/issues/8496)).
* Text:
* Add support for the SSA `primaryColour` style attribute
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
### 2.13.0 (not yet released - targeted for 2021-02-TBD) ### 2.13.0 (not yet released - targeted for 2021-02-TBD)

View File

@ -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 static com.google.android.exoplayer2.util.Util.castNonNull;
import android.text.Layout; import android.text.Layout;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
@ -301,7 +303,18 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
SsaStyle.Overrides styleOverrides, SsaStyle.Overrides styleOverrides,
float screenWidth, float screenWidth,
float screenHeight) { float screenHeight) {
Cue.Builder cue = new Cue.Builder().setText(text); SpannableString spannableText = new SpannableString(text);
Cue.Builder cue = new Cue.Builder().setText(spannableText);
if (style != null) {
if (style.primaryColor != null) {
spannableText.setSpan(
new ForegroundColorSpan(style.primaryColor),
/* start= */ 0,
/* end= */ spannableText.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
@SsaStyle.SsaAlignment int alignment; @SsaStyle.SsaAlignment int alignment;
if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {

View File

@ -17,16 +17,20 @@
package com.google.android.exoplayer2.text.ssa; package com.google.android.exoplayer2.text.ssa;
import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX; import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.graphics.Color;
import android.graphics.PointF; import android.graphics.PointF;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -85,15 +89,18 @@ import java.util.regex.Pattern;
public final String name; public final String name;
@SsaAlignment public final int alignment; @SsaAlignment public final int alignment;
@Nullable @ColorInt public final Integer primaryColor;
private SsaStyle(String name, @SsaAlignment int alignment) { private SsaStyle(
String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor) {
this.name = name; this.name = name;
this.alignment = alignment; this.alignment = alignment;
this.primaryColor = primaryColor;
} }
@Nullable @Nullable
public static SsaStyle fromStyleLine(String styleLine, Format format) { public static SsaStyle fromStyleLine(String styleLine, Format format) {
Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX)); checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ","); String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ",");
if (styleValues.length != format.length) { if (styleValues.length != format.length) {
Log.w( Log.w(
@ -105,7 +112,9 @@ import java.util.regex.Pattern;
} }
try { try {
return new SsaStyle( return new SsaStyle(
styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); styleValues[format.nameIndex].trim(),
parseAlignment(styleValues[format.alignmentIndex].trim()),
parseColor(styleValues[format.primaryColorIndex].trim()));
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
return null; return null;
@ -144,6 +153,44 @@ import java.util.regex.Pattern;
} }
} }
/**
* Parses a SSA V4+ color expression.
*
* <p>A SSA V4+ color can be represented in hex {@code ("&HAABBGGRR")} or in 64-bit decimal format
* (byte order AABBGGRR). In both cases the alpha channel's value needs to be inverted because in
* SSA the 0xFF alpha value means transparent and 0x00 means opaque which is the opposite from the
* Android {@link ColorInt} representation.
*
* @param ssaColorExpression A SSA V4+ color expression.
* @return The parsed color value, or null if parsing failed.
*/
@Nullable
@ColorInt
public static Integer parseColor(String ssaColorExpression) {
// We use a long because the value is an unsigned 32-bit number, so can be larger than
// Integer.MAX_VALUE.
long abgr;
try {
abgr =
ssaColorExpression.startsWith("&H")
// Parse color from hex format (&HAABBGGRR).
? Long.parseLong(ssaColorExpression.substring(2), /* radix= */ 16)
// Parse color from decimal format (bytes order AABBGGRR).
: Long.parseLong(ssaColorExpression);
// Ensure only the bottom 4 bytes of abgr are set.
checkArgument(abgr <= 0xFFFFFFFFL);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to parse color expression: '" + ssaColorExpression + "'", e);
return null;
}
// Convert ABGR to ARGB.
int a = Ints.checkedCast(((abgr >> 24) & 0xFF) ^ 0xFF); // Flip alpha.
int b = Ints.checkedCast((abgr >> 16) & 0xFF);
int g = Ints.checkedCast((abgr >> 8) & 0xFF);
int r = Ints.checkedCast(abgr & 0xFF);
return Color.argb(a, r, g, b);
}
/** /**
* Represents a {@code Format:} line from the {@code [V4+ Styles]} section * Represents a {@code Format:} line from the {@code [V4+ Styles]} section
* *
@ -154,11 +201,13 @@ import java.util.regex.Pattern;
public final int nameIndex; public final int nameIndex;
public final int alignmentIndex; public final int alignmentIndex;
public final int primaryColorIndex;
public final int length; 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.nameIndex = nameIndex;
this.alignmentIndex = alignmentIndex; this.alignmentIndex = alignmentIndex;
this.primaryColorIndex = primaryColorIndex;
this.length = length; this.length = length;
} }
@ -171,6 +220,7 @@ import java.util.regex.Pattern;
public static Format fromFormatLine(String styleFormatLine) { public static Format fromFormatLine(String styleFormatLine) {
int nameIndex = C.INDEX_UNSET; int nameIndex = C.INDEX_UNSET;
int alignmentIndex = C.INDEX_UNSET; int alignmentIndex = C.INDEX_UNSET;
int primaryColorIndex = C.INDEX_UNSET;
String[] keys = String[] keys =
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
for (int i = 0; i < keys.length; i++) { for (int i = 0; i < keys.length; i++) {
@ -181,9 +231,14 @@ import java.util.regex.Pattern;
case "alignment": case "alignment":
alignmentIndex = i; alignmentIndex = i;
break; 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;
} }
} }
@ -237,8 +292,7 @@ import java.util.regex.Pattern;
// Ignore invalid \pos() or \move() function. // Ignore invalid \pos() or \move() function.
} }
try { try {
@SsaAlignment @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents);
int parsedAlignment = parseAlignmentOverride(braceContents);
if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) { if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) {
alignment = parsedAlignment; alignment = parsedAlignment;
} }

View File

@ -18,10 +18,13 @@ package com.google.android.exoplayer2.text.ssa;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import android.graphics.Color;
import android.text.Layout; import android.text.Layout;
import android.text.Spanned;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.truth.SpannedSubject;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -44,6 +47,7 @@ public final class SsaDecoderTest {
private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes"; private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning"; private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning";
private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres"; private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres";
private static final String COLORS = "media/ssa/colors";
@Test @Test
public void decodeEmpty() throws IOException { public void decodeEmpty() throws IOException {
@ -267,6 +271,54 @@ public final class SsaDecoderTest {
assertTypicalCue3(subtitle, 0); assertTypicalCue3(subtitle, 0);
} }
@Test
public void decodeColors() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), COLORS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getEventTimeCount()).isEqualTo(14);
// &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB)
Spanned firstCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text;
SpannedSubject.assertThat(firstCueText)
.hasForegroundColorSpanBetween(0, firstCueText.length())
.withColor(Color.RED);
// &H0000FFFF (AABBGGRR) -> #FFFFFF00 (AARRGGBB)
Spanned secondCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text;
SpannedSubject.assertThat(secondCueText)
.hasForegroundColorSpanBetween(0, secondCueText.length())
.withColor(Color.YELLOW);
// &HFF00 (GGRR) -> #FF00FF00 (AARRGGBB)
Spanned thirdCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))).text;
SpannedSubject.assertThat(thirdCueText)
.hasForegroundColorSpanBetween(0, thirdCueText.length())
.withColor(Color.GREEN);
// &HA00000FF (AABBGGRR) -> #5FFF0000 (AARRGGBB)
Spanned fourthCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))).text;
SpannedSubject.assertThat(fourthCueText)
.hasForegroundColorSpanBetween(0, fourthCueText.length())
.withColor(0x5FFF0000);
// 16711680 (AABBGGRR) -> &H00FF0000 (AABBGGRR) -> #FF0000FF (AARRGGBB)
Spanned fifthCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))).text;
SpannedSubject.assertThat(fifthCueText)
.hasForegroundColorSpanBetween(0, fifthCueText.length())
.withColor(0xFF0000FF);
// 2164195328 (AABBGGRR) -> &H80FF0000 (AABBGGRR) -> #7F0000FF (AARRGGBB)
Spanned sixthCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))).text;
SpannedSubject.assertThat(sixthCueText)
.hasForegroundColorSpanBetween(0, sixthCueText.length())
.withColor(0x7F0000FF);
Spanned seventhCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))).text;
SpannedSubject.assertThat(seventhCueText)
.hasNoForegroundColorSpanBetween(0, seventhCueText.length());
}
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())

View File

@ -0,0 +1,26 @@
[Script Info]
Title: Coloring
Script Type: V4.00+
PlayResX: 1280
PlayResY: 720
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: PrimaryColourStyleHexRed ,Roboto,50,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleHexYellow ,Roboto,50,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleHexGreen ,Roboto,50,&HFF00 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleHexAlpha ,Roboto,50,&HA00000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleDecimal ,Roboto,50,16711680 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleDecimalAlpha ,Roboto,50,2164195328,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleInvalid ,Roboto,50,blue ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,Arnold,0,0,0,,First line in RED (&H000000FF).
Dialogue: 0,0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Arnold,0,0,0,,Second line in YELLOW (&H0000FFFF).
Dialogue: 0,0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,Third line in GREEN (leading zeros &HFF00).
Dialogue: 0,0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Arnold,0,0,0,,Fourth line in RED with alpha (&H400000FF).
Dialogue: 0,0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Arnold,0,0,0,,Fifth line in BLUE (16711680).
Dialogue: 0,0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha ,Arnold,0,0,0,,Sixth line in BLUE with alpha (2164195328).
Dialogue: 0,0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Arnold,0,0,0,,Seventh line with invalid color .