1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.text.subrip;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 
20 import androidx.test.core.app.ApplicationProvider;
21 import androidx.test.ext.junit.runners.AndroidJUnit4;
22 import com.google.android.exoplayer2.testutil.TestUtil;
23 import com.google.android.exoplayer2.text.Cue;
24 import com.google.android.exoplayer2.text.Subtitle;
25 import java.io.IOException;
26 import org.junit.Test;
27 import org.junit.runner.RunWith;
28 
29 /** Unit test for {@link SubripDecoder}. */
30 @RunWith(AndroidJUnit4.class)
31 public final class SubripDecoderTest {
32 
33   private static final String EMPTY_FILE = "subrip/empty";
34   private static final String TYPICAL_FILE = "subrip/typical";
35   private static final String TYPICAL_WITH_BYTE_ORDER_MARK = "subrip/typical_with_byte_order_mark";
36   private static final String TYPICAL_EXTRA_BLANK_LINE = "subrip/typical_extra_blank_line";
37   private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode";
38   private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence";
39   private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps";
40   private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end";
41   private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags";
42   private static final String TYPICAL_NO_HOURS_AND_MILLIS = "subrip/typical_no_hours_and_millis";
43 
44   @Test
decodeEmpty()45   public void decodeEmpty() throws IOException {
46     SubripDecoder decoder = new SubripDecoder();
47     byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
48     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
49 
50     assertThat(subtitle.getEventTimeCount()).isEqualTo(0);
51     assertThat(subtitle.getCues(0).isEmpty()).isTrue();
52   }
53 
54   @Test
decodeTypical()55   public void decodeTypical() throws IOException {
56     SubripDecoder decoder = new SubripDecoder();
57     byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
58     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
59 
60     assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
61     assertTypicalCue1(subtitle, 0);
62     assertTypicalCue2(subtitle, 2);
63     assertTypicalCue3(subtitle, 4);
64   }
65 
66   @Test
decodeTypicalWithByteOrderMark()67   public void decodeTypicalWithByteOrderMark() throws IOException {
68     SubripDecoder decoder = new SubripDecoder();
69     byte[] bytes =
70         TestUtil.getByteArray(
71             ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK);
72     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
73 
74     assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
75     assertTypicalCue1(subtitle, 0);
76     assertTypicalCue2(subtitle, 2);
77     assertTypicalCue3(subtitle, 4);
78   }
79 
80   @Test
decodeTypicalExtraBlankLine()81   public void decodeTypicalExtraBlankLine() throws IOException {
82     SubripDecoder decoder = new SubripDecoder();
83     byte[] bytes =
84         TestUtil.getByteArray(
85             ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE);
86     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
87 
88     assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
89     assertTypicalCue1(subtitle, 0);
90     assertTypicalCue2(subtitle, 2);
91     assertTypicalCue3(subtitle, 4);
92   }
93 
94   @Test
decodeTypicalMissingTimecode()95   public void decodeTypicalMissingTimecode() throws IOException {
96     // Parsing should succeed, parsing the first and third cues only.
97     SubripDecoder decoder = new SubripDecoder();
98     byte[] bytes =
99         TestUtil.getByteArray(
100             ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE);
101     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
102 
103     assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
104     assertTypicalCue1(subtitle, 0);
105     assertTypicalCue3(subtitle, 2);
106   }
107 
108   @Test
decodeTypicalMissingSequence()109   public void decodeTypicalMissingSequence() throws IOException {
110     // Parsing should succeed, parsing the first and third cues only.
111     SubripDecoder decoder = new SubripDecoder();
112     byte[] bytes =
113         TestUtil.getByteArray(
114             ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE);
115     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
116 
117     assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
118     assertTypicalCue1(subtitle, 0);
119     assertTypicalCue3(subtitle, 2);
120   }
121 
122   @Test
decodeTypicalNegativeTimestamps()123   public void decodeTypicalNegativeTimestamps() throws IOException {
124     // Parsing should succeed, parsing the third cue only.
125     SubripDecoder decoder = new SubripDecoder();
126     byte[] bytes =
127         TestUtil.getByteArray(
128             ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS);
129     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
130 
131     assertThat(subtitle.getEventTimeCount()).isEqualTo(2);
132     assertTypicalCue3(subtitle, 0);
133   }
134 
135   @Test
decodeTypicalUnexpectedEnd()136   public void decodeTypicalUnexpectedEnd() throws IOException {
137     // Parsing should succeed, parsing the first and second cues only.
138     SubripDecoder decoder = new SubripDecoder();
139     byte[] bytes =
140         TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END);
141     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
142 
143     assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
144     assertTypicalCue1(subtitle, 0);
145     assertTypicalCue2(subtitle, 2);
146   }
147 
148   @Test
decodeCueWithTag()149   public void decodeCueWithTag() throws IOException {
150     SubripDecoder decoder = new SubripDecoder();
151     byte[] bytes =
152         TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
153     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
154 
155     assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString())
156         .isEqualTo("This is the first subtitle.");
157 
158     assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString())
159         .isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
160 
161     assertThat(subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString())
162         .isEqualTo("This is the third subtitle.");
163 
164     assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString())
165         .isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket.");
166 
167     assertThat(subtitle.getCues(subtitle.getEventTime(8)).get(0).text.toString())
168         .isEqualTo("This is the fifth subtitle with multiple valid tags.");
169 
170     assertAlignmentCue(subtitle, 10, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_START); // {/an1}
171     assertAlignmentCue(subtitle, 12, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_MIDDLE); // {/an2}
172     assertAlignmentCue(subtitle, 14, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_END); // {/an3}
173     assertAlignmentCue(subtitle, 16, Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_START); // {/an4}
174     assertAlignmentCue(subtitle, 18, Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_MIDDLE); // {/an5}
175     assertAlignmentCue(subtitle, 20, Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_END); // {/an6}
176     assertAlignmentCue(subtitle, 22, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_START); // {/an7}
177     assertAlignmentCue(subtitle, 24, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_MIDDLE); // {/an8}
178     assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9}
179   }
180 
181   @Test
decodeTypicalNoHoursAndMillis()182   public void decodeTypicalNoHoursAndMillis() throws IOException {
183     SubripDecoder decoder = new SubripDecoder();
184     byte[] bytes =
185         TestUtil.getByteArray(
186             ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
187     Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
188 
189     assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
190     assertTypicalCue1(subtitle, 0);
191     assertThat(subtitle.getEventTime(2)).isEqualTo(2_000_000);
192     assertThat(subtitle.getEventTime(3)).isEqualTo(3_000_000);
193     assertTypicalCue3(subtitle, 4);
194   }
195 
assertTypicalCue1(Subtitle subtitle, int eventIndex)196   private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
197     assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
198     assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
199         .isEqualTo("This is the first subtitle.");
200     assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1234000);
201   }
202 
assertTypicalCue2(Subtitle subtitle, int eventIndex)203   private static void assertTypicalCue2(Subtitle subtitle, int eventIndex) {
204     assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2345000);
205     assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
206         .isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
207     assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3456000);
208   }
209 
assertTypicalCue3(Subtitle subtitle, int eventIndex)210   private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) {
211     long expectedStartTimeUs = (((2L * 60L * 60L) + 4L) * 1000L + 567L) * 1000L;
212     assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(expectedStartTimeUs);
213     assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
214         .isEqualTo("This is the third subtitle.");
215     long expectedEndTimeUs = (((2L * 60L * 60L) + 8L) * 1000L + 901L) * 1000L;
216     assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(expectedEndTimeUs);
217   }
218 
assertAlignmentCue( Subtitle subtitle, int eventIndex, @Cue.AnchorType int lineAnchor, @Cue.AnchorType int positionAnchor)219   private static void assertAlignmentCue(
220       Subtitle subtitle,
221       int eventIndex,
222       @Cue.AnchorType int lineAnchor,
223       @Cue.AnchorType int positionAnchor) {
224     long eventTimeUs = subtitle.getEventTime(eventIndex);
225     Cue cue = subtitle.getCues(eventTimeUs).get(0);
226     assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
227     assertThat(cue.lineAnchor).isEqualTo(lineAnchor);
228     assertThat(cue.line).isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(lineAnchor));
229     assertThat(cue.positionAnchor).isEqualTo(positionAnchor);
230     assertThat(cue.position)
231         .isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(positionAnchor));
232   }
233 }
234