diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java
index 2cf7703e6c..c59cded4c3 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java
@@ -28,7 +28,7 @@ import androidx.media3.extractor.text.ssa.SsaParser;
import androidx.media3.extractor.text.subrip.SubripParser;
import androidx.media3.extractor.text.ttml.TtmlDecoder;
import androidx.media3.extractor.text.tx3g.Tx3gDecoder;
-import androidx.media3.extractor.text.webvtt.Mp4WebvttDecoder;
+import androidx.media3.extractor.text.webvtt.Mp4WebvttParser;
import androidx.media3.extractor.text.webvtt.WebvttDecoder;
/** A factory for {@link SubtitleDecoder} instances. */
@@ -60,7 +60,7 @@ public interface SubtitleDecoderFactory {
*
*
* - WebVTT ({@link WebvttDecoder})
- *
- WebVTT (MP4) ({@link Mp4WebvttDecoder})
+ *
- WebVTT (MP4) ({@link Mp4WebvttParser})
*
- TTML ({@link TtmlDecoder})
*
- SubRip ({@link SubripParser})
*
- SSA/ASS ({@link SsaParser})
@@ -104,7 +104,8 @@ public interface SubtitleDecoderFactory {
"DelegatingSubtitleDecoderWithSsaParser",
new SsaParser(format.initializationData));
case MimeTypes.APPLICATION_MP4VTT:
- return new Mp4WebvttDecoder();
+ return new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithMp4WebvttParser", new Mp4WebvttParser());
case MimeTypes.APPLICATION_TTML:
return new TtmlDecoder();
case MimeTypes.APPLICATION_SUBRIP:
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/Mp4WebvttDecoderTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithMp4WebvttParserTest.java
similarity index 81%
rename from libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/Mp4WebvttDecoderTest.java
rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithMp4WebvttParserTest.java
index 936694bada..e7618a3703 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/Mp4WebvttDecoderTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithMp4WebvttParserTest.java
@@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.media3.extractor.text.webvtt;
+package androidx.media3.exoplayer.text;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
import androidx.media3.common.text.Cue;
import androidx.media3.extractor.text.Subtitle;
-import androidx.media3.extractor.text.SubtitleDecoderException;
+import androidx.media3.extractor.text.webvtt.Mp4WebvttParser;
+import androidx.media3.extractor.text.webvtt.WebvttCueParser;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.truth.Expect;
import java.util.List;
@@ -28,9 +29,9 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link Mp4WebvttDecoder}. */
+/** Unit test for a {@link DelegatingSubtitleDecoder} backed by {@link Mp4WebvttParser}. */
@RunWith(AndroidJUnit4.class)
-public final class Mp4WebvttDecoderTest {
+public final class DelegatingSubtitleDecoderWithMp4WebvttParserTest {
private static final byte[] SINGLE_CUE_SAMPLE = {
0x00,
@@ -161,8 +162,10 @@ public final class Mp4WebvttDecoderTest {
// Positive tests.
@Test
- public void singleCueSample() throws SubtitleDecoderException {
- Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
+ public void singleCueSample() {
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithMp4WebvttParser", new Mp4WebvttParser());
Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false);
// Line feed must be trimmed by the decoder
Cue expectedCue = WebvttCueParser.newCueForText("Hello World");
@@ -170,8 +173,10 @@ public final class Mp4WebvttDecoderTest {
}
@Test
- public void twoCuesSample() throws SubtitleDecoderException {
- Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
+ public void twoCuesSample() {
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithMp4WebvttParser", new Mp4WebvttParser());
Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false);
Cue firstExpectedCue = WebvttCueParser.newCueForText("Hello World");
Cue secondExpectedCue = WebvttCueParser.newCueForText("Bye Bye");
@@ -179,25 +184,24 @@ public final class Mp4WebvttDecoderTest {
}
@Test
- public void noCueSample() throws SubtitleDecoderException {
- Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
+ public void noCueSample() {
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithMp4WebvttParser", new Mp4WebvttParser());
Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false);
- assertThat(result.getEventTimeCount()).isEqualTo(1);
- assertThat(result.getEventTime(0)).isEqualTo(0);
- assertThat(result.getCues(0)).isEmpty();
+ assertThat(result.getEventTimeCount()).isEqualTo(0);
}
// Negative tests.
@Test
public void sampleWithIncompleteHeader() {
- Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
- try {
- decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length, false);
- } catch (SubtitleDecoderException e) {
- return;
- }
- fail();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithMp4WebvttParser", new Mp4WebvttParser());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length, false));
}
// Util methods
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java
similarity index 64%
rename from libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttDecoder.java
rename to libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java
index 977eede122..5820e0add5 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttDecoder.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java
@@ -15,22 +15,25 @@
*/
package androidx.media3.extractor.text.webvtt;
+import static androidx.media3.common.util.Assertions.checkArgument;
+
import androidx.annotation.Nullable;
+import androidx.media3.common.C;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
-import androidx.media3.extractor.text.SimpleSubtitleDecoder;
-import androidx.media3.extractor.text.Subtitle;
-import androidx.media3.extractor.text.SubtitleDecoderException;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.media3.extractor.text.SubtitleParser;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */
+/** A {@link SubtitleParser} for Webvtt embedded in a Mp4 container file. */
@SuppressWarnings("ConstantField")
@UnstableApi
-public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
+public final class Mp4WebvttParser implements SubtitleParser {
private static final int BOX_HEADER_SIZE = 8;
@@ -44,43 +47,55 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
private static final int TYPE_vttc = 0x76747463;
private final ParsableByteArray sampleData;
+ private byte[] dataScratch = Util.EMPTY_BYTE_ARRAY;
- public Mp4WebvttDecoder() {
- super("Mp4WebvttDecoder");
+ public Mp4WebvttParser() {
sampleData = new ParsableByteArray();
}
@Override
- protected Subtitle decode(byte[] data, int length, boolean reset)
- throws SubtitleDecoderException {
- // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:
- // first 4 bytes size and then 4 bytes type.
- sampleData.reset(data, length);
- List resultingCueList = new ArrayList<>();
- while (sampleData.bytesLeft() > 0) {
- if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
- throw new SubtitleDecoderException("Incomplete Mp4Webvtt Top Level box header found.");
+ public ImmutableList parse(byte[] data, int offset, int length) {
+ if (offset != 0) {
+ if (dataScratch.length < length) {
+ dataScratch = new byte[length];
}
+ System.arraycopy(
+ /* src= */ data, /* scrPos= */ offset, /* dest= */ dataScratch, /* destPos= */ 0, length);
+ sampleData.reset(dataScratch, length);
+ } else {
+ sampleData.reset(data, length);
+ }
+ List cues = new ArrayList<>();
+ while (sampleData.bytesLeft() > 0) {
+ // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box
+ // parsing: first 4 bytes size and then 4 bytes type.
+ checkArgument(
+ sampleData.bytesLeft() >= BOX_HEADER_SIZE,
+ "Incomplete Mp4Webvtt Top Level box header found.");
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
if (boxType == TYPE_vttc) {
- resultingCueList.add(parseVttCueBox(sampleData, boxSize - BOX_HEADER_SIZE));
+ cues.add(parseVttCueBox(sampleData, boxSize - BOX_HEADER_SIZE));
} else {
// Peers of the VTTCueBox are still not supported and are skipped.
sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
}
}
- return new Mp4WebvttSubtitle(resultingCueList);
+ return cues.isEmpty()
+ ? ImmutableList.of()
+ : ImmutableList.of(
+ new CuesWithTiming(cues, /* startTimeUs= */ 0, /* durationUs= */ C.TIME_UNSET));
}
- private static Cue parseVttCueBox(ParsableByteArray sampleData, int remainingCueBoxBytes)
- throws SubtitleDecoderException {
+ @Override
+ public void reset() {}
+
+ private static Cue parseVttCueBox(ParsableByteArray sampleData, int remainingCueBoxBytes) {
@Nullable Cue.Builder cueBuilder = null;
@Nullable CharSequence cueText = null;
while (remainingCueBoxBytes > 0) {
- if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
- throw new SubtitleDecoderException("Incomplete vtt cue box header found.");
- }
+ checkArgument(
+ remainingCueBoxBytes >= BOX_HEADER_SIZE, "Incomplete vtt cue box header found.");
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
remainingCueBoxBytes -= BOX_HEADER_SIZE;
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttSubtitle.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttSubtitle.java
deleted file mode 100644
index bfa9163887..0000000000
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttSubtitle.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.media3.extractor.text.webvtt;
-
-import androidx.media3.common.C;
-import androidx.media3.common.text.Cue;
-import androidx.media3.common.util.Assertions;
-import androidx.media3.extractor.text.Subtitle;
-import java.util.Collections;
-import java.util.List;
-
-/** Representation of a Webvtt subtitle embedded in a MP4 container file. */
-/* package */ final class Mp4WebvttSubtitle implements Subtitle {
-
- private final List cues;
-
- public Mp4WebvttSubtitle(List cueList) {
- cues = Collections.unmodifiableList(cueList);
- }
-
- @Override
- public int getNextEventTimeIndex(long timeUs) {
- return timeUs < 0 ? 0 : C.INDEX_UNSET;
- }
-
- @Override
- public int getEventTimeCount() {
- return 1;
- }
-
- @Override
- public long getEventTime(int index) {
- Assertions.checkArgument(index == 0);
- return 0;
- }
-
- @Override
- public List getCues(long timeUs) {
- return timeUs >= 0 ? cues : Collections.emptyList();
- }
-}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCueParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCueParser.java
index ff6d0fd298..cbf44a94b6 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCueParser.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCueParser.java
@@ -15,6 +15,7 @@
*/
package androidx.media3.extractor.text.webvtt;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -36,6 +37,7 @@ import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.HorizontalTextInVerticalContextSpan;
import androidx.media3.common.text.RubySpan;
@@ -230,7 +232,8 @@ public final class WebvttCueParser {
}
/** Create a new {@link Cue} containing {@code text} and with WebVTT default values. */
- /* package */ static Cue newCueForText(CharSequence text) {
+ @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+ public static Cue newCueForText(CharSequence text) {
WebvttCueInfoBuilder infoBuilder = new WebvttCueInfoBuilder();
infoBuilder.text = text;
return infoBuilder.toCueBuilder().build();
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParserTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParserTest.java
new file mode 100644
index 0000000000..a090c2ef59
--- /dev/null
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParserTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.extractor.text.webvtt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import androidx.media3.common.text.Cue;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Expect;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link Mp4WebvttParser}. */
+@RunWith(AndroidJUnit4.class)
+public final class Mp4WebvttParserTest {
+
+ private static final byte[] SINGLE_CUE_SAMPLE = {
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x1C, // Size
+ 0x76,
+ 0x74,
+ 0x74,
+ 0x63, // "vttc" Box type. VTT Cue box begins:
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x14, // Contained payload box's size
+ 0x70,
+ 0x61,
+ 0x79,
+ 0x6c, // Contained payload box's type (payl), Cue Payload Box begins:
+ 0x48,
+ 0x65,
+ 0x6c,
+ 0x6c,
+ 0x6f,
+ 0x20,
+ 0x57,
+ 0x6f,
+ 0x72,
+ 0x6c,
+ 0x64,
+ 0x0a // Hello World\n
+ };
+
+ private static final byte[] DOUBLE_CUE_SAMPLE = {
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x1B, // Size
+ 0x76,
+ 0x74,
+ 0x74,
+ 0x63, // "vttc" Box type. First VTT Cue box begins:
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x13, // First contained payload box's size
+ 0x70,
+ 0x61,
+ 0x79,
+ 0x6c, // First contained payload box's type (payl), Cue Payload Box begins:
+ 0x48,
+ 0x65,
+ 0x6c,
+ 0x6c,
+ 0x6f,
+ 0x20,
+ 0x57,
+ 0x6f,
+ 0x72,
+ 0x6c,
+ 0x64, // Hello World
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x17, // Size
+ 0x76,
+ 0x74,
+ 0x74,
+ 0x63, // "vttc" Box type. Second VTT Cue box begins:
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0F, // Contained payload box's size
+ 0x70,
+ 0x61,
+ 0x79,
+ 0x6c, // Contained payload box's type (payl), Payload begins:
+ 0x42,
+ 0x79,
+ 0x65,
+ 0x20,
+ 0x42,
+ 0x79,
+ 0x65 // Bye Bye
+ };
+
+ private static final byte[] NO_CUE_SAMPLE = {
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x1B, // Size
+ 0x74,
+ 0x74,
+ 0x74,
+ 0x63, // "tttc" Box type, which is not a Cue. Should be skipped:
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x13, // Contained payload box's size
+ 0x70,
+ 0x61,
+ 0x79,
+ 0x6c, // Contained payload box's type (payl), Cue Payload Box begins:
+ 0x48,
+ 0x65,
+ 0x6c,
+ 0x6c,
+ 0x6f,
+ 0x20,
+ 0x57,
+ 0x6f,
+ 0x72,
+ 0x6c,
+ 0x64 // Hello World
+ };
+
+ private static final byte[] INCOMPLETE_HEADER_SAMPLE = {
+ 0x00, 0x00, 0x00, 0x23, // Size
+ 0x76, 0x74, 0x74, 0x63, // "vttc" Box type. VTT Cue box begins:
+ 0x00, 0x00, 0x00, 0x14, // Contained payload box's size
+ 0x70, 0x61, 0x79, 0x6c, // Contained payload box's type (payl), Cue Payload Box begins:
+ 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a, // Hello World\n
+ 0x00, 0x00, 0x00, 0x07, // Size of an incomplete header, which belongs to the first vttc box.
+ 0x76, 0x74, 0x74
+ };
+
+ @Rule public final Expect expect = Expect.create();
+
+ // Positive tests.
+
+ @Test
+ public void singleCueSample() {
+ Mp4WebvttParser parser = new Mp4WebvttParser();
+ List result = parser.parse(SINGLE_CUE_SAMPLE);
+ // Line feed must be trimmed by the decoder
+ Cue expectedCue = WebvttCueParser.newCueForText("Hello World");
+ assertMp4WebvttSubtitleEquals(result, expectedCue);
+ }
+
+ @Test
+ public void twoCuesSample() {
+ Mp4WebvttParser parser = new Mp4WebvttParser();
+ List result = parser.parse(DOUBLE_CUE_SAMPLE);
+ Cue firstExpectedCue = WebvttCueParser.newCueForText("Hello World");
+ Cue secondExpectedCue = WebvttCueParser.newCueForText("Bye Bye");
+ assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue);
+ }
+
+ @Test
+ public void noCueSample() {
+ Mp4WebvttParser parser = new Mp4WebvttParser();
+ List result = parser.parse(NO_CUE_SAMPLE);
+ assertThat(result).isEmpty();
+ }
+
+ // Negative tests.
+
+ @Test
+ public void sampleWithIncompleteHeader() {
+ Mp4WebvttParser parser = new Mp4WebvttParser();
+ assertThrows(IllegalArgumentException.class, () -> parser.parse(INCOMPLETE_HEADER_SAMPLE));
+ }
+
+ // Util methods
+
+ /**
+ * Asserts that the Subtitle's cues (which are all part of the event at t=0) are equal to the
+ * expected Cues.
+ *
+ * @param cuesWithTimings The list of {@link CuesWithTiming} to check.
+ * @param expectedCues The expected {@link Cue}s.
+ */
+ private void assertMp4WebvttSubtitleEquals(
+ List cuesWithTimings, Cue... expectedCues) {
+ assertThat(cuesWithTimings).hasSize(1);
+ assertThat(cuesWithTimings.get(0).startTimeUs).isEqualTo(0);
+ ImmutableList allCues = cuesWithTimings.get(0).cues;
+ assertThat(allCues).hasSize(expectedCues.length);
+ for (int i = 0; i < allCues.size(); i++) {
+ assertCuesEqual(expectedCues[i], allCues.get(i));
+ }
+ }
+
+ /** Asserts that two cues are equal. */
+ private void assertCuesEqual(Cue expected, Cue actual) {
+ expect.withMessage("Cue.line").that(actual.line).isEqualTo(expected.line);
+ expect.withMessage("Cue.lineAnchor").that(actual.lineAnchor).isEqualTo(expected.lineAnchor);
+ expect.withMessage("Cue.lineType").that(actual.lineType).isEqualTo(expected.lineType);
+ expect.withMessage("Cue.position").that(actual.position).isEqualTo(expected.position);
+ expect
+ .withMessage("Cue.positionAnchor")
+ .that(actual.positionAnchor)
+ .isEqualTo(expected.positionAnchor);
+ expect.withMessage("Cue.size").that(actual.size).isEqualTo(expected.size);
+ expect.withMessage("Cue.text").that(actual.text.toString()).isEqualTo(expected.text.toString());
+ expect
+ .withMessage("Cue.textAlignment")
+ .that(actual.textAlignment)
+ .isEqualTo(expected.textAlignment);
+
+ assertThat(expect.hasFailures()).isFalse();
+ }
+}