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.webvtt;
17 
18 import android.text.TextUtils;
19 import androidx.annotation.Nullable;
20 import com.google.android.exoplayer2.ParserException;
21 import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
22 import com.google.android.exoplayer2.text.Subtitle;
23 import com.google.android.exoplayer2.text.SubtitleDecoderException;
24 import com.google.android.exoplayer2.util.ParsableByteArray;
25 import java.util.ArrayList;
26 import java.util.List;
27 
28 /**
29  * A {@link SimpleSubtitleDecoder} for WebVTT.
30  * <p>
31  * @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
32  */
33 public final class WebvttDecoder extends SimpleSubtitleDecoder {
34 
35   private static final int EVENT_NONE = -1;
36   private static final int EVENT_END_OF_FILE = 0;
37   private static final int EVENT_COMMENT = 1;
38   private static final int EVENT_STYLE_BLOCK = 2;
39   private static final int EVENT_CUE = 3;
40 
41   private static final String COMMENT_START = "NOTE";
42   private static final String STYLE_START = "STYLE";
43 
44   private final ParsableByteArray parsableWebvttData;
45   private final CssParser cssParser;
46 
WebvttDecoder()47   public WebvttDecoder() {
48     super("WebvttDecoder");
49     parsableWebvttData = new ParsableByteArray();
50     cssParser = new CssParser();
51   }
52 
53   @Override
decode(byte[] bytes, int length, boolean reset)54   protected Subtitle decode(byte[] bytes, int length, boolean reset)
55       throws SubtitleDecoderException {
56     parsableWebvttData.reset(bytes, length);
57     List<WebvttCssStyle> definedStyles = new ArrayList<>();
58 
59     // Validate the first line of the header, and skip the remainder.
60     try {
61       WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
62     } catch (ParserException e) {
63       throw new SubtitleDecoderException(e);
64     }
65     while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
66 
67     int event;
68     List<WebvttCueInfo> cueInfos = new ArrayList<>();
69     while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) {
70       if (event == EVENT_COMMENT) {
71         skipComment(parsableWebvttData);
72       } else if (event == EVENT_STYLE_BLOCK) {
73         if (!cueInfos.isEmpty()) {
74           throw new SubtitleDecoderException("A style block was found after the first cue.");
75         }
76         parsableWebvttData.readLine(); // Consume the "STYLE" header.
77         definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
78       } else if (event == EVENT_CUE) {
79         @Nullable
80         WebvttCueInfo cueInfo = WebvttCueParser.parseCue(parsableWebvttData, definedStyles);
81         if (cueInfo != null) {
82           cueInfos.add(cueInfo);
83         }
84       }
85     }
86     return new WebvttSubtitle(cueInfos);
87   }
88 
89   /**
90    * Positions the input right before the next event, and returns the kind of event found. Does not
91    * consume any data from such event, if any.
92    *
93    * @return The kind of event found.
94    */
getNextEvent(ParsableByteArray parsableWebvttData)95   private static int getNextEvent(ParsableByteArray parsableWebvttData) {
96     int foundEvent = EVENT_NONE;
97     int currentInputPosition = 0;
98     while (foundEvent == EVENT_NONE) {
99       currentInputPosition = parsableWebvttData.getPosition();
100       String line = parsableWebvttData.readLine();
101       if (line == null) {
102         foundEvent = EVENT_END_OF_FILE;
103       } else if (STYLE_START.equals(line)) {
104         foundEvent = EVENT_STYLE_BLOCK;
105       } else if (line.startsWith(COMMENT_START)) {
106         foundEvent = EVENT_COMMENT;
107       } else {
108         foundEvent = EVENT_CUE;
109       }
110     }
111     parsableWebvttData.setPosition(currentInputPosition);
112     return foundEvent;
113   }
114 
skipComment(ParsableByteArray parsableWebvttData)115   private static void skipComment(ParsableByteArray parsableWebvttData) {
116     while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
117   }
118 
119 }
120