From cf9772414886e13ea8ebae58290e8c2499704fde Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sat, 9 Apr 2022 22:20:47 +0200 Subject: [PATCH 1/6] Parse and use margin values --- demos/main/src/main/assets/media.exolist.json | 7 + .../exoplayer2/text/ssa/SsaDecoder.java | 128 +++++++++++------- .../text/ssa/SsaDialogueFormat.java | 42 +++++- .../android/exoplayer2/text/ssa/SsaStyle.java | 55 +++++++- 4 files changed, 178 insertions(+), 54 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 09688fa73ac..a487383774c 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -625,6 +625,13 @@ "subtitle_mime_type": "text/x-ssa", "subtitle_language": "en" }, + { + "name": "SubStation Margin", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", + "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/ef81209e8a50874179672173dc106120a7ee8814/test-subs-margin.ass", + "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/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c6df62bb5f5..6a9236d54ed 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -273,7 +273,19 @@ private void parseDialogueLine( .replace("\\N", "\n") .replace("\\n", "\n") .replace("\\h", "\u00A0"); - Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight); + float marginLeft = SsaStyle.parseMargin(lineValues[format.marginLeftIndex]); + float marginRight = SsaStyle.parseMargin(lineValues[format.marginRightIndex]); + float marginVertical = SsaStyle.parseMargin(lineValues[format.marginVerticalIndex]); + + Cue cue = createCue( + text, + style, + styleOverrides, + marginLeft, + marginRight, + marginVertical, + screenWidth, + screenHeight); int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues); int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues); @@ -306,58 +318,14 @@ private static Cue createCue( String text, @Nullable SsaStyle style, SsaStyle.Overrides styleOverrides, + float marginLeft, + float marginRight, + float marginVertical, float screenWidth, float screenHeight) { 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); - } - if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) { - cue.setTextSize( - style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); - } - if (style.bold && style.italic) { - spannableText.setSpan( - new StyleSpan(Typeface.BOLD_ITALIC), - /* start= */ 0, - /* end= */ spannableText.length(), - SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - } else if (style.bold) { - spannableText.setSpan( - new StyleSpan(Typeface.BOLD), - /* start= */ 0, - /* end= */ spannableText.length(), - SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - } else if (style.italic) { - spannableText.setSpan( - new StyleSpan(Typeface.ITALIC), - /* start= */ 0, - /* end= */ spannableText.length(), - SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - } - if (style.underline) { - spannableText.setSpan( - new UnderlineSpan(), - /* start= */ 0, - /* end= */ spannableText.length(), - SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - } - if (style.strikeout) { - spannableText.setSpan( - new StrikethroughSpan(), - /* start= */ 0, - /* end= */ spannableText.length(), - SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - @SsaStyle.SsaAlignment int alignment; if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { alignment = styleOverrides.alignment; @@ -376,11 +344,73 @@ private static Cue createCue( cue.setPosition(styleOverrides.position.x / screenWidth); cue.setLine(styleOverrides.position.y / screenHeight, LINE_TYPE_FRACTION); } else { - // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines. cue.setPosition(computeDefaultLineOrPosition(cue.getPositionAnchor())); cue.setLine(computeDefaultLineOrPosition(cue.getLineAnchor()), LINE_TYPE_FRACTION); } + cue.setPosition(cue.getPosition() + marginLeft / screenWidth); + cue.setPosition(cue.getPosition() - marginRight / screenWidth); + cue.setLine(cue.getLine() - marginVertical / screenHeight, LINE_TYPE_FRACTION); + + if (style == null) { + return cue.build(); + } + // Apply styles. + if (style.primaryColor != null) { + spannableText.setSpan( + new ForegroundColorSpan(style.primaryColor), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) { + cue.setTextSize( + style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); + } + if (style.bold && style.italic) { + spannableText.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (style.bold) { + spannableText.setSpan( + new StyleSpan(Typeface.BOLD), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (style.italic) { + spannableText.setSpan( + new StyleSpan(Typeface.ITALIC), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.underline) { + spannableText.setSpan( + new UnderlineSpan(), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.strikeout) { + spannableText.setSpan( + new StrikethroughSpan(), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } + // Margins from dialogue line takes precedence over the style margins. + cue.setPosition(cue.getPosition() + (marginLeft != 0 + ? marginLeft / screenWidth + : style.marginLeft / screenWidth)); + cue.setPosition(cue.getPosition() - (marginRight != 0 + ? marginRight / screenWidth + : style.marginRight / screenWidth)); + cue.setLine(cue.getLine() - (marginVertical != 0 + ? marginVertical / screenHeight + : style.marginVertical / screenHeight), LINE_TYPE_FRACTION); + return cue.build(); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java index 82f3dd642c9..8e04fc0df5f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java @@ -36,14 +36,27 @@ public final int endTimeIndex; public final int styleIndex; public final int textIndex; + public final int marginLeftIndex; + public final int marginRightIndex; + public final int marginVerticalIndex; public final int length; private SsaDialogueFormat( - int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) { + int startTimeIndex, + int endTimeIndex, + int styleIndex, + int textIndex, + int marginLeftIndex, + int marginRightIndex, + int marginVerticalIndex, + int length) { this.startTimeIndex = startTimeIndex; this.endTimeIndex = endTimeIndex; this.styleIndex = styleIndex; this.textIndex = textIndex; + this.marginLeftIndex = marginLeftIndex; + this.marginRightIndex = marginRightIndex; + this.marginVerticalIndex = marginVerticalIndex; this.length = length; } @@ -58,6 +71,9 @@ public static SsaDialogueFormat fromFormatLine(String formatLine) { int endTimeIndex = C.INDEX_UNSET; int styleIndex = C.INDEX_UNSET; int textIndex = C.INDEX_UNSET; + int marginLeftIndex = C.INDEX_UNSET; + int marginRightIndex = C.INDEX_UNSET; + int marginVerticalIndex = C.INDEX_UNSET; Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); for (int i = 0; i < keys.length; i++) { @@ -74,12 +90,32 @@ public static SsaDialogueFormat fromFormatLine(String formatLine) { case "text": textIndex = i; break; + case "marginl": + marginLeftIndex = i; + break; + case "marginr": + marginRightIndex = i; + break; + case "marginv": + marginVerticalIndex = i; + break; } } return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET - && textIndex != C.INDEX_UNSET) - ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) + && textIndex != C.INDEX_UNSET + && marginLeftIndex != C.INDEX_UNSET + && marginRightIndex != C.INDEX_UNSET + && marginVerticalIndex != C.INDEX_UNSET) + ? new SsaDialogueFormat( + startTimeIndex, + endTimeIndex, + styleIndex, + textIndex, + marginLeftIndex, + marginRightIndex, + marginVerticalIndex, + keys.length) : null; } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index 8e09291312c..3771777ce6e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -100,6 +100,9 @@ public final boolean italic; public final boolean underline; public final boolean strikeout; + public final float marginLeft; + public final float marginRight; + public final float marginVertical; private SsaStyle( String name, @@ -109,7 +112,10 @@ private SsaStyle( boolean bold, boolean italic, boolean underline, - boolean strikeout) { + boolean strikeout, + float marginLeft, + float marginRight, + float marginVertical) { this.name = name; this.alignment = alignment; this.primaryColor = primaryColor; @@ -118,6 +124,9 @@ private SsaStyle( this.italic = italic; this.underline = underline; this.strikeout = strikeout; + this.marginLeft = marginLeft; + this.marginRight = marginRight; + this.marginVertical = marginVertical; } @Nullable @@ -151,7 +160,16 @@ && parseBooleanValue(styleValues[format.italicIndex].trim()), format.underlineIndex != C.INDEX_UNSET && parseBooleanValue(styleValues[format.underlineIndex].trim()), format.strikeoutIndex != C.INDEX_UNSET - && parseBooleanValue(styleValues[format.strikeoutIndex].trim())); + && parseBooleanValue(styleValues[format.strikeoutIndex].trim()), + format.marginLeftIndex != C.INDEX_UNSET + ? parseMargin(styleValues[format.marginLeftIndex].trim()) + : Cue.DIMEN_UNSET, + format.marginRightIndex != C.INDEX_UNSET + ? parseMargin(styleValues[format.marginRightIndex].trim()) + : Cue.DIMEN_UNSET, + format.marginVerticalIndex != C.INDEX_UNSET + ? parseMargin(styleValues[format.marginVerticalIndex].trim()) + : Cue.DIMEN_UNSET); } catch (RuntimeException e) { Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); return null; @@ -227,6 +245,15 @@ public static Integer parseColor(String ssaColorExpression) { return Color.argb(a, r, g, b); } + public static float parseMargin(String floatValue) { + try { + return Float.parseFloat(floatValue); + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse margin value: '" + floatValue + "'", e); + return 0f; + } + } + private static float parseFontSize(String fontSize) { try { return Float.parseFloat(fontSize); @@ -262,6 +289,9 @@ private static boolean parseBooleanValue(String booleanValue) { public final int italicIndex; public final int underlineIndex; public final int strikeoutIndex; + public final int marginLeftIndex; + public final int marginRightIndex; + public final int marginVerticalIndex; public final int length; private Format( @@ -273,6 +303,9 @@ private Format( int italicIndex, int underlineIndex, int strikeoutIndex, + int marginLeftIndex, + int marginRightIndex, + int marginVerticalIndex, int length) { this.nameIndex = nameIndex; this.alignmentIndex = alignmentIndex; @@ -282,6 +315,9 @@ private Format( this.italicIndex = italicIndex; this.underlineIndex = underlineIndex; this.strikeoutIndex = strikeoutIndex; + this.marginLeftIndex = marginLeftIndex; + this.marginRightIndex = marginRightIndex; + this.marginVerticalIndex = marginVerticalIndex; this.length = length; } @@ -300,6 +336,9 @@ public static Format fromFormatLine(String styleFormatLine) { int italicIndex = C.INDEX_UNSET; int underlineIndex = C.INDEX_UNSET; int strikeoutIndex = C.INDEX_UNSET; + int marginLeftIndex = C.INDEX_UNSET; + int marginRightIndex = C.INDEX_UNSET; + int marginVerticalIndex = C.INDEX_UNSET; String[] keys = TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); for (int i = 0; i < keys.length; i++) { @@ -328,6 +367,15 @@ public static Format fromFormatLine(String styleFormatLine) { case "strikeout": strikeoutIndex = i; break; + case "marginl": + marginLeftIndex = i; + break; + case "marginr": + marginRightIndex = i; + break; + case "marginv": + marginVerticalIndex = i; + break; } } return nameIndex != C.INDEX_UNSET @@ -340,6 +388,9 @@ public static Format fromFormatLine(String styleFormatLine) { italicIndex, underlineIndex, strikeoutIndex, + marginLeftIndex, + marginRightIndex, + marginVerticalIndex, keys.length) : null; } From 9e680738465a072cc34f6822113cff2bac0391ff Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sat, 9 Apr 2022 23:37:25 +0200 Subject: [PATCH 2/6] Fine tune margin style for SSA subs --- .../exoplayer2/text/ssa/SsaDecoder.java | 42 +++++++++++-------- .../text/ssa/SsaDialogueFormat.java | 5 +-- .../test/assets/media/ssa/invalid_positioning | 2 +- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 6a9236d54ed..74738fe0900 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -273,9 +273,13 @@ private void parseDialogueLine( .replace("\\N", "\n") .replace("\\n", "\n") .replace("\\h", "\u00A0"); - float marginLeft = SsaStyle.parseMargin(lineValues[format.marginLeftIndex]); - float marginRight = SsaStyle.parseMargin(lineValues[format.marginRightIndex]); - float marginVertical = SsaStyle.parseMargin(lineValues[format.marginVerticalIndex]); + + float marginLeft = format.marginLeftIndex != C.INDEX_UNSET + ? SsaStyle.parseMargin(lineValues[format.marginLeftIndex]) : 0f; + float marginRight = format.marginRightIndex != C.INDEX_UNSET + ? SsaStyle.parseMargin(lineValues[format.marginRightIndex]) : 0f; + float marginVertical = format.marginVerticalIndex != C.INDEX_UNSET + ? SsaStyle.parseMargin(lineValues[format.marginVerticalIndex]) : 0f; Cue cue = createCue( text, @@ -348,14 +352,28 @@ private static Cue createCue( cue.setLine(computeDefaultLineOrPosition(cue.getLineAnchor()), LINE_TYPE_FRACTION); } - cue.setPosition(cue.getPosition() + marginLeft / screenWidth); - cue.setPosition(cue.getPosition() - marginRight / screenWidth); - cue.setLine(cue.getLine() - marginVertical / screenHeight, LINE_TYPE_FRACTION); + // Apply margins if there are no overrides and we have valid positions. + if (styleOverrides.alignment == SsaStyle.SSA_ALIGNMENT_UNKNOWN + && styleOverrides.position == null + && cue.getPosition() != Cue.DIMEN_UNSET + && cue.getLine() != Cue.DIMEN_UNSET) { + // Margin from Dialogue lines takes precedence over margin from Style line. + cue.setPosition(cue.getPosition() + (marginLeft != 0f + ? marginLeft / screenWidth + : style != null ? style.marginLeft / screenWidth : 0f)); + cue.setPosition(cue.getPosition() - (marginRight != 0f + ? marginRight / screenWidth + : style != null ? style.marginRight / screenWidth : 0f)); + cue.setLine(cue.getLine() - (marginVertical != 0f + ? marginVertical / screenHeight + : style != null ? style.marginVertical / screenHeight : 0f), LINE_TYPE_FRACTION); + } if (style == null) { return cue.build(); } - // Apply styles. + + // Apply rest of the styles. if (style.primaryColor != null) { spannableText.setSpan( new ForegroundColorSpan(style.primaryColor), @@ -400,16 +418,6 @@ private static Cue createCue( /* end= */ spannableText.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); } - // Margins from dialogue line takes precedence over the style margins. - cue.setPosition(cue.getPosition() + (marginLeft != 0 - ? marginLeft / screenWidth - : style.marginLeft / screenWidth)); - cue.setPosition(cue.getPosition() - (marginRight != 0 - ? marginRight / screenWidth - : style.marginRight / screenWidth)); - cue.setLine(cue.getLine() - (marginVertical != 0 - ? marginVertical / screenHeight - : style.marginVertical / screenHeight), LINE_TYPE_FRACTION); return cue.build(); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java index 8e04fc0df5f..0028c0c4a71 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java @@ -103,10 +103,7 @@ public static SsaDialogueFormat fromFormatLine(String formatLine) { } return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET - && textIndex != C.INDEX_UNSET - && marginLeftIndex != C.INDEX_UNSET - && marginRightIndex != C.INDEX_UNSET - && marginVerticalIndex != C.INDEX_UNSET) + && textIndex != C.INDEX_UNSET) ? new SsaDialogueFormat( startTimeIndex, endTimeIndex, diff --git a/testdata/src/test/assets/media/ssa/invalid_positioning b/testdata/src/test/assets/media/ssa/invalid_positioning index ade4cce9c47..9b5ca30f9a7 100644 --- a/testdata/src/test/assets/media/ssa/invalid_positioning +++ b/testdata/src/test/assets/media/ssa/invalid_positioning @@ -6,7 +6,7 @@ PlayResY: 200 [V4+ Styles] ! Alignment is set to 4 - i.e. middle-left 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: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,0,1 [Events] Format: Layer, Start, End, Style, Name, Text From a073ea41718a907a0de9a603e3dad1228ddc5e70 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sun, 10 Apr 2022 00:36:41 +0200 Subject: [PATCH 3/6] Add unit tests for SSA margin styles --- demos/main/src/main/assets/media.exolist.json | 4 +- .../exoplayer2/text/ssa/SsaDecoderTest.java | 43 +++++++++++++++++++ .../src/test/assets/media/ssa/style_margin | 23 ++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 testdata/src/test/assets/media/ssa/style_margin diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index a487383774c..6d0c7d89df8 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -626,9 +626,9 @@ "subtitle_language": "en" }, { - "name": "SubStation Margin", + "name": "SubStation Alpha Margin", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", - "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/ef81209e8a50874179672173dc106120a7ee8814/test-subs-margin.ass", + "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/c376e7c67946a3e350473bfb3869554d2125c9ef/test-subs-margin.ass", "subtitle_mime_type": "text/x-ssa", "subtitle_language": "en" }, diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 4b3b8cef64e..fc07ca2b0de 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -53,6 +53,7 @@ public final class SsaDecoderTest { private static final String STYLE_BOLD_ITALIC = "media/ssa/style_bold_italic"; private static final String STYLE_UNDERLINE = "media/ssa/style_underline"; private static final String STYLE_STRIKEOUT = "media/ssa/style_strikeout"; + private static final String STYLE_MARGIN = "media/ssa/style_margin"; @Test public void decodeEmpty() throws IOException { @@ -412,6 +413,48 @@ public void decodeStrikeout() throws IOException { .hasNoStrikethroughSpanBetween(0, secondCueText.length()); } + @Test + public void decodeMargins() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_MARGIN); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + // Margin left. + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.position).isEqualTo(0.6f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); + // Margin right. + Cue secondClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondClue.position).isEqualTo(0.4f); + assertThat(secondClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(secondClue.line).isEqualTo(0.95f); + // Margin vertical. + Cue thirdClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdClue.position).isEqualTo(0.5f); + assertThat(thirdClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(thirdClue.line).isEqualTo(0.75f); + // Margin left + vertical. + Cue fourthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthClue.position).isEqualTo(0.6f); + assertThat(fourthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(fourthClue.line).isEqualTo(0.75f); + // Margin left + vertical (defined in Dialogue). + Cue fifthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthClue.position).isEqualTo(0.4f); + assertThat(fifthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(fifthClue.line).isEqualTo(0.75f); + // Margin should be skipped because of the {\pos} override. + Cue sixthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthClue.position).isEqualTo(0.5f); + assertThat(sixthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(sixthClue.line).isEqualTo(0.25f); + // Margin should be skipped because of the {\an} override. + Cue seventhClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))); + assertThat(seventhClue.position).isEqualTo(0.5f); + assertThat(seventhClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(seventhClue.line).isEqualTo(0.5f); + } + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) diff --git a/testdata/src/test/assets/media/ssa/style_margin b/testdata/src/test/assets/media/ssa/style_margin new file mode 100644 index 00000000000..3550dc338ba --- /dev/null +++ b/testdata/src/test/assets/media/ssa/style_margin @@ -0,0 +1,23 @@ +[Script Info] +Title: SSA/ASS Test +Original Script: Arnold Szabo +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: MarginLeft ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,0 ,1 +Style: MarginRight ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,128,0 ,1 +Style: MarginVertical ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,0,144 ,1 +Style: MarginMixed ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,144 ,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:00:00.95,0:00:02.11,MarginLeft ,Arnold,0,0,0,, Line with margin left +Dialogue: 0,0:00:03.50,0:00:05.50,MarginRight ,Arnold,0,0,0,, Line with margin right +Dialogue: 0,0:00:07.50,0:00:10.00,MarginVertical ,Arnold,0,0,0,, Line with margin vertical +Dialogue: 0,0:00:11.50,0:00:14.00,MarginMixed ,Arnold,0,0,0,, Line with margin vertical and left +Dialogue: 0,0:00:15.50,0:00:17.00,MarginLeft ,Arnold,-128,0,144,,Line margin defined in dialogue +Dialogue: 0,0:00:18.50,0:00:20.00,MarginLeft ,Arnold,200,0,0,, {\pos(640,180)} Line with fixed position +Dialogue: 0,0:00:21.00,0:00:22.00,MarginLeft ,Arnold,0,200,0,, {\an5} Line with alignment override From c743fb7c1e2ddf80df3bec53e4e568c854ee303c Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sat, 16 Apr 2022 19:22:09 +0200 Subject: [PATCH 4/6] Correct SSA/ASS margin style behavior when the alignment is middle/top --- demos/main/src/main/assets/media.exolist.json | 2 +- .../exoplayer2/text/ssa/SsaDecoder.java | 11 +++++--- .../android/exoplayer2/text/ssa/SsaStyle.java | 27 +++++++++++++++++++ .../exoplayer2/text/ssa/SsaDecoderTest.java | 10 +++++++ .../src/test/assets/media/ssa/style_margin | 26 ++++++++++-------- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 6d0c7d89df8..72acce8289d 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -628,7 +628,7 @@ { "name": "SubStation Alpha Margin", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", - "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/c376e7c67946a3e350473bfb3869554d2125c9ef/test-subs-margin.ass", + "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/b93916ec9ca9145b6e47107c4c357abe43bc5641/test-subs-margin.ass", "subtitle_mime_type": "text/x-ssa", "subtitle_language": "en" }, diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 74738fe0900..7cce2d43adc 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -364,9 +364,14 @@ private static Cue createCue( cue.setPosition(cue.getPosition() - (marginRight != 0f ? marginRight / screenWidth : style != null ? style.marginRight / screenWidth : 0f)); - cue.setLine(cue.getLine() - (marginVertical != 0f - ? marginVertical / screenHeight - : style != null ? style.marginVertical / screenHeight : 0f), LINE_TYPE_FRACTION); + // Ignore vertical margin if alignment is middle. + if (!SsaStyle.hasMiddleAlignment(style)) { + float verticalMargin = marginVertical != 0f ? marginVertical / screenHeight + : style != null ? style.marginVertical / screenHeight : 0f; + // Apply margin from top if alignment is top. + cue.setLine(cue.getLine() - (SsaStyle.hasTopAlignment(style) + ? -verticalMargin : verticalMargin), LINE_TYPE_FRACTION); + } } if (style == null) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index 3771777ce6e..44435c8c10c 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -176,6 +176,33 @@ && parseBooleanValue(styleValues[format.strikeoutIndex].trim()), } } + public static boolean hasMiddleAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_MIDDLE_LEFT + || style.alignment == SSA_ALIGNMENT_MIDDLE_CENTER + || style.alignment == SSA_ALIGNMENT_MIDDLE_RIGHT; + } + + public static boolean hasTopAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_TOP_LEFT + || style.alignment == SSA_ALIGNMENT_TOP_CENTER + || style.alignment == SSA_ALIGNMENT_TOP_RIGHT; + } + + public static boolean hasBottomAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_BOTTOM_LEFT + || style.alignment == SSA_ALIGNMENT_BOTTOM_CENTER + || style.alignment == SSA_ALIGNMENT_BOTTOM_RIGHT; + } + private static @SsaAlignment int parseAlignment(String alignmentStr) { try { @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index fc07ca2b0de..13018784200 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -453,6 +453,16 @@ public void decodeMargins() throws IOException { assertThat(seventhClue.position).isEqualTo(0.5f); assertThat(seventhClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); assertThat(seventhClue.line).isEqualTo(0.5f); + // Margin should be skipped because of middle alignment. + Cue eighthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14))); + assertThat(eighthClue.position).isEqualTo(0.95f); + assertThat(eighthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(eighthClue.line).isEqualTo(0.5f); + // Margin applied from top because of top alignment. + Cue ninthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(16))); + assertThat(ninthClue.position).isEqualTo(0.95f); + assertThat(ninthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(ninthClue.line).isEqualTo(0.25f); } private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { diff --git a/testdata/src/test/assets/media/ssa/style_margin b/testdata/src/test/assets/media/ssa/style_margin index 3550dc338ba..59cb9a9c103 100644 --- a/testdata/src/test/assets/media/ssa/style_margin +++ b/testdata/src/test/assets/media/ssa/style_margin @@ -7,17 +7,21 @@ 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: MarginLeft ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,0 ,1 -Style: MarginRight ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,128,0 ,1 -Style: MarginVertical ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,0,144 ,1 -Style: MarginMixed ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,144 ,1 +Style: MarginLeft ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,0 ,1 +Style: MarginRight ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,128,0 ,1 +Style: MarginVertical ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,0,144 ,1 +Style: MarginMixed ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,144 ,1 +Style: MarginVerticalAn6 ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,6, 0,0,144 ,1 +Style: MarginVerticalAn9 ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,9, 0,0,144 ,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text -Dialogue: 0,0:00:00.95,0:00:02.11,MarginLeft ,Arnold,0,0,0,, Line with margin left -Dialogue: 0,0:00:03.50,0:00:05.50,MarginRight ,Arnold,0,0,0,, Line with margin right -Dialogue: 0,0:00:07.50,0:00:10.00,MarginVertical ,Arnold,0,0,0,, Line with margin vertical -Dialogue: 0,0:00:11.50,0:00:14.00,MarginMixed ,Arnold,0,0,0,, Line with margin vertical and left -Dialogue: 0,0:00:15.50,0:00:17.00,MarginLeft ,Arnold,-128,0,144,,Line margin defined in dialogue -Dialogue: 0,0:00:18.50,0:00:20.00,MarginLeft ,Arnold,200,0,0,, {\pos(640,180)} Line with fixed position -Dialogue: 0,0:00:21.00,0:00:22.00,MarginLeft ,Arnold,0,200,0,, {\an5} Line with alignment override +Dialogue: 0,0:00:00.95,0:00:02.11,MarginLeft ,Arnold,0,0,0,, Line with margin left +Dialogue: 0,0:00:03.50,0:00:05.50,MarginRight ,Arnold,0,0,0,, Line with margin right +Dialogue: 0,0:00:07.50,0:00:10.00,MarginVertical ,Arnold,0,0,0,, Line with margin vertical +Dialogue: 0,0:00:11.50,0:00:14.00,MarginMixed ,Arnold,0,0,0,, Line with margin vertical and left +Dialogue: 0,0:00:15.50,0:00:17.00,MarginLeft ,Arnold,-128,0,144,, Line margin defined in dialogue +Dialogue: 0,0:00:18.50,0:00:20.00,MarginLeft ,Arnold,200,0,0,, {\pos(640,180)} Line with fixed position +Dialogue: 0,0:00:21.00,0:00:22.00,MarginLeft ,Arnold,0,200,0,, {\an5} Line with alignment override +Dialogue: 0,0:00:23.00,0:00:25.00,MarginVerticalAn6 ,Arnold,0,0,0,, Alignment middle, ignore vertical margin +Dialogue: 0,0:00:26.00,0:00:27.00,MarginVerticalAn9 ,Arnold,0,0,0,, Alignment top, apply margin from top \ No newline at end of file From e30396b1e7e1cf700bf28da32e53d97087551b3f Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sun, 17 Apr 2022 22:37:16 +0200 Subject: [PATCH 5/6] Correct margin calculation for SSA/ASS subtitles --- demos/main/src/main/assets/media.exolist.json | 4 +- .../exoplayer2/text/ssa/SsaDecoder.java | 55 ++++++++----- .../android/exoplayer2/text/ssa/SsaStyle.java | 81 ++++++++++++------- .../exoplayer2/text/ssa/SsaDecoderTest.java | 74 +++++++++++------ .../src/test/assets/media/ssa/style_margin | 33 ++++---- 5 files changed, 157 insertions(+), 90 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 72acce8289d..4fe10806e12 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -626,9 +626,9 @@ "subtitle_language": "en" }, { - "name": "SubStation Alpha Margin", + "name": "SubStation Alpha margin", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", - "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/b93916ec9ca9145b6e47107c4c357abe43bc5641/test-subs-margin.ass", + "subtitle_uri": "https://gist.githubusercontent.com/szaboa/bca7cdc90c1492eb747032f267a5de19/raw/8985eeb544641e174da22a1dafe50cd87393512f/test-subs-margin.ass", "subtitle_mime_type": "text/x-ssa", "subtitle_language": "en" }, diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 7cce2d43adc..5b8f7e452b8 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -274,20 +274,20 @@ private void parseDialogueLine( .replace("\\n", "\n") .replace("\\h", "\u00A0"); - float marginLeft = format.marginLeftIndex != C.INDEX_UNSET + float dialogueMarginLeft = format.marginLeftIndex != C.INDEX_UNSET ? SsaStyle.parseMargin(lineValues[format.marginLeftIndex]) : 0f; - float marginRight = format.marginRightIndex != C.INDEX_UNSET + float dialogueMarginRight = format.marginRightIndex != C.INDEX_UNSET ? SsaStyle.parseMargin(lineValues[format.marginRightIndex]) : 0f; - float marginVertical = format.marginVerticalIndex != C.INDEX_UNSET + float dialogueMarginVertical = format.marginVerticalIndex != C.INDEX_UNSET ? SsaStyle.parseMargin(lineValues[format.marginVerticalIndex]) : 0f; Cue cue = createCue( text, style, styleOverrides, - marginLeft, - marginRight, - marginVertical, + dialogueMarginLeft, + dialogueMarginRight, + dialogueMarginVertical, screenWidth, screenHeight); @@ -322,9 +322,9 @@ private static Cue createCue( String text, @Nullable SsaStyle style, SsaStyle.Overrides styleOverrides, - float marginLeft, - float marginRight, - float marginVertical, + float dialogueMarginLeft, + float dialogueMarginRight, + float dialogueMarginVertical, float screenWidth, float screenHeight) { SpannableString spannableText = new SpannableString(text); @@ -357,20 +357,35 @@ private static Cue createCue( && styleOverrides.position == null && cue.getPosition() != Cue.DIMEN_UNSET && cue.getLine() != Cue.DIMEN_UNSET) { + // Margin from Dialogue lines takes precedence over margin from Style line. - cue.setPosition(cue.getPosition() + (marginLeft != 0f - ? marginLeft / screenWidth - : style != null ? style.marginLeft / screenWidth : 0f)); - cue.setPosition(cue.getPosition() - (marginRight != 0f - ? marginRight / screenWidth - : style != null ? style.marginRight / screenWidth : 0f)); - // Ignore vertical margin if alignment is middle. + float marginLeft = dialogueMarginLeft != 0f + ? dialogueMarginLeft / screenWidth + : style != null ? style.marginLeft / screenWidth : 0f; + float marginRight = dialogueMarginRight != 0f + ? dialogueMarginRight / screenWidth + : style != null ? style.marginRight / screenWidth : 0f; + + // Apply margin left, margin right. + if (SsaStyle.hasLeftAlignment(style)) { + cue.setPosition(cue.getPosition() + marginLeft); + cue.setSize(1 - marginRight - marginLeft); + } else if (SsaStyle.hasRightAlignment(style)) { + cue.setPosition(cue.getPosition() - marginRight); + cue.setSize(1 - marginRight - marginLeft); + } else { + // Center alignment or unknown. + cue.setPosition(cue.getPosition() + (marginLeft - marginRight) / 2); + cue.setSize(1 - marginRight - marginLeft); + } + + // Apply margin vertical, ignore it when alignment is middle. if (!SsaStyle.hasMiddleAlignment(style)) { - float verticalMargin = marginVertical != 0f ? marginVertical / screenHeight + float marginVertical = dialogueMarginVertical != 0f ? dialogueMarginVertical / screenHeight : style != null ? style.marginVertical / screenHeight : 0f; - // Apply margin from top if alignment is top. - cue.setLine(cue.getLine() - (SsaStyle.hasTopAlignment(style) - ? -verticalMargin : verticalMargin), LINE_TYPE_FRACTION); + cue.setLine( + cue.getLine() - (SsaStyle.hasTopAlignment(style) ? -marginVertical : marginVertical), + LINE_TYPE_FRACTION); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index 44435c8c10c..61097582fc3 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -176,33 +176,6 @@ && parseBooleanValue(styleValues[format.strikeoutIndex].trim()), } } - public static boolean hasMiddleAlignment(@Nullable SsaStyle style) { - if (style == null) { - return false; - } - return style.alignment == SSA_ALIGNMENT_MIDDLE_LEFT - || style.alignment == SSA_ALIGNMENT_MIDDLE_CENTER - || style.alignment == SSA_ALIGNMENT_MIDDLE_RIGHT; - } - - public static boolean hasTopAlignment(@Nullable SsaStyle style) { - if (style == null) { - return false; - } - return style.alignment == SSA_ALIGNMENT_TOP_LEFT - || style.alignment == SSA_ALIGNMENT_TOP_CENTER - || style.alignment == SSA_ALIGNMENT_TOP_RIGHT; - } - - public static boolean hasBottomAlignment(@Nullable SsaStyle style) { - if (style == null) { - return false; - } - return style.alignment == SSA_ALIGNMENT_BOTTOM_LEFT - || style.alignment == SSA_ALIGNMENT_BOTTOM_CENTER - || style.alignment == SSA_ALIGNMENT_BOTTOM_RIGHT; - } - private static @SsaAlignment int parseAlignment(String alignmentStr) { try { @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); @@ -300,6 +273,60 @@ private static boolean parseBooleanValue(String booleanValue) { } } + public static boolean hasMiddleAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_MIDDLE_LEFT + || style.alignment == SSA_ALIGNMENT_MIDDLE_CENTER + || style.alignment == SSA_ALIGNMENT_MIDDLE_RIGHT; + } + + public static boolean hasTopAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_TOP_LEFT + || style.alignment == SSA_ALIGNMENT_TOP_CENTER + || style.alignment == SSA_ALIGNMENT_TOP_RIGHT; + } + + public static boolean hasBottomAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_BOTTOM_LEFT + || style.alignment == SSA_ALIGNMENT_BOTTOM_CENTER + || style.alignment == SSA_ALIGNMENT_BOTTOM_RIGHT; + } + + public static boolean hasLeftAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_TOP_LEFT + || style.alignment == SSA_ALIGNMENT_MIDDLE_LEFT + || style.alignment == SSA_ALIGNMENT_BOTTOM_LEFT; + } + + public static boolean hasRightAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_TOP_RIGHT + || style.alignment == SSA_ALIGNMENT_MIDDLE_RIGHT + || style.alignment == SSA_ALIGNMENT_BOTTOM_RIGHT; + } + + public static boolean hasCenterAlignment(@Nullable SsaStyle style) { + if (style == null) { + return false; + } + return style.alignment == SSA_ALIGNMENT_TOP_CENTER + || style.alignment == SSA_ALIGNMENT_MIDDLE_CENTER + || style.alignment == SSA_ALIGNMENT_BOTTOM_CENTER; + } + /** * Represents a {@code Format:} line from the {@code [V4+ Styles]} section * diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 13018784200..1fd60459783 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -418,51 +418,73 @@ public void decodeMargins() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_MARGIN); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); - // Margin left. + + // PlayResX=1280px, PlayResY=720px + + // Alignment 1, position anchor = start, position = (0.05f, 0.95f) + // margin_left = 128px = 0.1f, margin_right 256px = 0.2f Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertThat(firstCue.position).isEqualTo(0.6f); + assertThat(firstCue.position).isEqualTo(0.15f); // = 0.05f + margin_left assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); assertThat(firstCue.line).isEqualTo(0.95f); - // Margin right. + assertThat(firstCue.size).isEqualTo(0.7f); // = 1 - margin_right - margin_left + + // Alignment 6, position anchor = end, position = (0.95f, 0.5f) + // margin_left = 128px = 0.1f, margin_right = 256px = 0.2f Cue secondClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertThat(secondClue.position).isEqualTo(0.4f); + assertThat(secondClue.position).isEqualTo(0.75f); // = 1 - margin_right assertThat(secondClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(secondClue.line).isEqualTo(0.95f); - // Margin vertical. + assertThat(secondClue.line).isEqualTo(0.5f); + assertThat(secondClue.size).isEqualTo(0.7f); // = 1 - margin_right - margin_left + + // Alignment 2, position anchor = middle, position = (0.5f, 0.95f) + // margin_left = 128px = 0.1f, margin_right = 256px = 0.2f Cue thirdClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); - assertThat(thirdClue.position).isEqualTo(0.5f); + assertThat(thirdClue.position).isEqualTo(0.45f); // 0.5f + (margin_left - margin_right)/2 assertThat(thirdClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(thirdClue.line).isEqualTo(0.75f); - // Margin left + vertical. + assertThat(thirdClue.line).isEqualTo(0.95f); + assertThat(thirdClue.size).isEqualTo(0.7f); // = 1 - margin_right - margin_left + + // Alignment 5, position anchor = middle, position = (0.5f, 0.5f) + // margin_vertical = 144px = 0.2f but needs to be ignored when alignment is middle [4,5,6] Cue fourthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); - assertThat(fourthClue.position).isEqualTo(0.6f); + assertThat(fourthClue.position).isEqualTo(0.5f); assertThat(fourthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(fourthClue.line).isEqualTo(0.75f); - // Margin left + vertical (defined in Dialogue). + assertThat(fourthClue.line).isEqualTo(0.5f); + + // Alignment 2, position anchor = middle, position = (0.5f, 0.95f) + // margin_vertical = 144px = 0.2f, to be applied from bottom when alignment is bottom [1,2,3] Cue fifthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); - assertThat(fifthClue.position).isEqualTo(0.4f); + assertThat(fifthClue.position).isEqualTo(0.5f); assertThat(fifthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(fifthClue.line).isEqualTo(0.75f); - // Margin should be skipped because of the {\pos} override. + assertThat(fifthClue.line).isEqualTo(0.75f); // = 0.95f - margin_vertical + + // Alignment 9, position anchor = end, position = (0.95f, 0.05f) + // margin_vertical = 144px = 0.2f, to be applied from top when alignment is top [7,8,9] Cue sixthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); - assertThat(sixthClue.position).isEqualTo(0.5f); + assertThat(sixthClue.position).isEqualTo(0.95f); // alignment 9 assertThat(sixthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(sixthClue.line).isEqualTo(0.25f); - // Margin should be skipped because of the {\an} override. + assertThat(sixthClue.line).isEqualTo(0.25f); // = 0.05f + margin_vertical + + // Alignment 2, position anchor = middle, position = (0.5f, 0.95f) + // margin_left = 128px = 0.1f, margin_vertical = 144px = 0.2f, margin_right = 0f (from Dialogue) Cue seventhClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))); - assertThat(seventhClue.position).isEqualTo(0.5f); + assertThat(seventhClue.position).isEqualTo(0.55f); // 0.5f + (margin_left - margin_right)/2 assertThat(seventhClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(seventhClue.line).isEqualTo(0.5f); - // Margin should be skipped because of middle alignment. + assertThat(seventhClue.line).isEqualTo(0.75f); // 0.95 - margin_vertical + assertThat(seventhClue.size).isEqualTo(0.9f); // 1 - margin_right - margin_left + + // Position override {\pos(640,180)} -> ignore margins Cue eighthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14))); - assertThat(eighthClue.position).isEqualTo(0.95f); + assertThat(eighthClue.position).isEqualTo(0.5f); assertThat(eighthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(eighthClue.line).isEqualTo(0.5f); - // Margin applied from top because of top alignment. + assertThat(eighthClue.line).isEqualTo(0.25f); + + // Alignment override {\an5}, position = (0.5f, 0.5f) -> ignore margins Cue ninthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(16))); - assertThat(ninthClue.position).isEqualTo(0.95f); + assertThat(ninthClue.position).isEqualTo(0.5f); assertThat(ninthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(ninthClue.line).isEqualTo(0.25f); + assertThat(ninthClue.line).isEqualTo(0.5f); } private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { diff --git a/testdata/src/test/assets/media/ssa/style_margin b/testdata/src/test/assets/media/ssa/style_margin index 59cb9a9c103..4ccdcc95ec2 100644 --- a/testdata/src/test/assets/media/ssa/style_margin +++ b/testdata/src/test/assets/media/ssa/style_margin @@ -7,21 +7,24 @@ 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: MarginLeft ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,0 ,1 -Style: MarginRight ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,128,0 ,1 -Style: MarginVertical ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,0,144 ,1 -Style: MarginMixed ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,0,144 ,1 -Style: MarginVerticalAn6 ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,6, 0,0,144 ,1 -Style: MarginVerticalAn9 ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,9, 0,0,144 ,1 +Style: AlignmentLeft ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,1, 128,256,0 ,1 +Style: AlignmentRight ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,6, 128,256,0 ,1 +Style: AlignmentCenter ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,256,0 ,1 +Style: AlignmentMiddle ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,5, 0,0,144 ,1 +Style: AlignmentBottom ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,0,144 ,1 +Style: AlignmentTop ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,9, 0,0,144 ,1 +Style: DialogueMargin ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 0,0,0 ,1 +Style: PositionOverride ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,144,0 ,1 +Style: AlignmentOverride ,Roboto,30,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2, 128,144,0 ,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text -Dialogue: 0,0:00:00.95,0:00:02.11,MarginLeft ,Arnold,0,0,0,, Line with margin left -Dialogue: 0,0:00:03.50,0:00:05.50,MarginRight ,Arnold,0,0,0,, Line with margin right -Dialogue: 0,0:00:07.50,0:00:10.00,MarginVertical ,Arnold,0,0,0,, Line with margin vertical -Dialogue: 0,0:00:11.50,0:00:14.00,MarginMixed ,Arnold,0,0,0,, Line with margin vertical and left -Dialogue: 0,0:00:15.50,0:00:17.00,MarginLeft ,Arnold,-128,0,144,, Line margin defined in dialogue -Dialogue: 0,0:00:18.50,0:00:20.00,MarginLeft ,Arnold,200,0,0,, {\pos(640,180)} Line with fixed position -Dialogue: 0,0:00:21.00,0:00:22.00,MarginLeft ,Arnold,0,200,0,, {\an5} Line with alignment override -Dialogue: 0,0:00:23.00,0:00:25.00,MarginVerticalAn6 ,Arnold,0,0,0,, Alignment middle, ignore vertical margin -Dialogue: 0,0:00:26.00,0:00:27.00,MarginVerticalAn9 ,Arnold,0,0,0,, Alignment top, apply margin from top \ No newline at end of file +Dialogue: 0,0:00:00.95,0:00:02.11,AlignmentLeft ,Arnold,0,0,0,, Margin with alignment left - long text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text +Dialogue: 0,0:00:02.20,0:00:03.40,AlignmentRight ,Arnold,0,0,0,, Margin with alignment right - long text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text +Dialogue: 0,0:00:03.45,0:00:04.40,AlignmentCenter ,Arnold,0,0,0,, Margin with alignment center - long text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text ong text long text long text long text long text long text long text text long text long text +Dialogue: 0,0:00:04.50,0:00:06.50,AlignmentMiddle ,Arnold,0,0,0,, Middle alignment - ignore vertical margin +Dialogue: 0,0:00:07.50,0:00:10.00,AlignmentBottom ,Arnold,0,0,0,, Bottom alignment - apply vertical margin from bottom +Dialogue: 0,0:00:11.50,0:00:14.00,AlignmentTop ,Arnold,0,0,0,, Top alignment - apply vertical margin from top +Dialogue: 0,0:00:15.50,0:00:17.00,DialogueMargin ,Arnold,128,0,144,, Margin defined in dialogue +Dialogue: 0,0:00:18.50,0:00:20.00,PositionOverride ,Arnold,0,0,0,, {\pos(640,180)} Position override - ignore margins +Dialogue: 0,0:00:21.00,0:00:22.00,AlignmentOverride ,Arnold,0,0,0,, {\an5} Alignment override - ignore margins \ No newline at end of file From e8a23963a1e97ed8e30e878d620795abfb06d00b Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sun, 17 Apr 2022 22:43:10 +0200 Subject: [PATCH 6/6] Update comments in SsaDecoderTests --- .../exoplayer2/text/ssa/SsaDecoderTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 1fd60459783..cc42a5e5e4a 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -419,7 +419,7 @@ public void decodeMargins() throws IOException { byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_MARGIN); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); - // PlayResX=1280px, PlayResY=720px + // PlayResX = 1280px, PlayResY = 720px // Alignment 1, position anchor = start, position = (0.05f, 0.95f) // margin_left = 128px = 0.1f, margin_right 256px = 0.2f @@ -427,15 +427,15 @@ public void decodeMargins() throws IOException { assertThat(firstCue.position).isEqualTo(0.15f); // = 0.05f + margin_left assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); assertThat(firstCue.line).isEqualTo(0.95f); - assertThat(firstCue.size).isEqualTo(0.7f); // = 1 - margin_right - margin_left + assertThat(firstCue.size).isEqualTo(0.7f); // = 1f - margin_right - margin_left // Alignment 6, position anchor = end, position = (0.95f, 0.5f) // margin_left = 128px = 0.1f, margin_right = 256px = 0.2f Cue secondClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertThat(secondClue.position).isEqualTo(0.75f); // = 1 - margin_right + assertThat(secondClue.position).isEqualTo(0.75f); // = 1f - margin_right assertThat(secondClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); assertThat(secondClue.line).isEqualTo(0.5f); - assertThat(secondClue.size).isEqualTo(0.7f); // = 1 - margin_right - margin_left + assertThat(secondClue.size).isEqualTo(0.7f); // = 1f - margin_right - margin_left // Alignment 2, position anchor = middle, position = (0.5f, 0.95f) // margin_left = 128px = 0.1f, margin_right = 256px = 0.2f @@ -443,7 +443,7 @@ public void decodeMargins() throws IOException { assertThat(thirdClue.position).isEqualTo(0.45f); // 0.5f + (margin_left - margin_right)/2 assertThat(thirdClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); assertThat(thirdClue.line).isEqualTo(0.95f); - assertThat(thirdClue.size).isEqualTo(0.7f); // = 1 - margin_right - margin_left + assertThat(thirdClue.size).isEqualTo(0.7f); // = 1f - margin_right - margin_left // Alignment 5, position anchor = middle, position = (0.5f, 0.5f) // margin_vertical = 144px = 0.2f but needs to be ignored when alignment is middle [4,5,6] @@ -462,7 +462,7 @@ public void decodeMargins() throws IOException { // Alignment 9, position anchor = end, position = (0.95f, 0.05f) // margin_vertical = 144px = 0.2f, to be applied from top when alignment is top [7,8,9] Cue sixthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); - assertThat(sixthClue.position).isEqualTo(0.95f); // alignment 9 + assertThat(sixthClue.position).isEqualTo(0.95f); assertThat(sixthClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); assertThat(sixthClue.line).isEqualTo(0.25f); // = 0.05f + margin_vertical @@ -471,8 +471,8 @@ public void decodeMargins() throws IOException { Cue seventhClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))); assertThat(seventhClue.position).isEqualTo(0.55f); // 0.5f + (margin_left - margin_right)/2 assertThat(seventhClue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertThat(seventhClue.line).isEqualTo(0.75f); // 0.95 - margin_vertical - assertThat(seventhClue.size).isEqualTo(0.9f); // 1 - margin_right - margin_left + assertThat(seventhClue.line).isEqualTo(0.75f); // 0.95f - margin_vertical + assertThat(seventhClue.size).isEqualTo(0.9f); // 1f - margin_right - margin_left // Position override {\pos(640,180)} -> ignore margins Cue eighthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14)));