[Mac] process raw VTT in-band captions
[WebKit-https.git] / Source / WebCore / html / track / WebVTTParser.cpp
1 /*
2  * Copyright (C) 2011, 2013 Google Inc.  All rights reserved.
3  * Copyright (C) 2013 Cable Television Labs, Inc.
4  * Copyright (C) 2011-2014 Apple Inc.  All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are
8  * met:
9  *
10  *     * Redistributions of source code must retain the above copyright
11  * notice, this list of conditions and the following disclaimer.
12  *     * Redistributions in binary form must reproduce the above
13  * copyright notice, this list of conditions and the following disclaimer
14  * in the documentation and/or other materials provided with the
15  * distribution.
16  *     * Neither the name of Google Inc. nor the names of its
17  * contributors may be used to endorse or promote products derived from
18  * this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32
33 #include "config.h"
34
35 #if ENABLE(VIDEO_TRACK)
36
37 #include "WebVTTParser.h"
38
39 #include "ISOVTTCue.h"
40 #include "ProcessingInstruction.h"
41 #include "Text.h"
42 #include "VTTScanner.h"
43 #include "WebVTTElement.h"
44
45 namespace WebCore {
46
47 const double secondsPerHour = 3600;
48 const double secondsPerMinute = 60;
49 const double secondsPerMillisecond = 0.001;
50 const char* fileIdentifier = "WEBVTT";
51 const unsigned fileIdentifierLength = 6;
52
53 #if ENABLE(WEBVTT_REGIONS)
54 bool WebVTTParser::parseFloatPercentageValue(VTTScanner& valueScanner, float& percentage)
55 {
56     float number;
57     if (!valueScanner.scanFloat(number))
58         return false;
59     // '%' must be present and at the end of the setting value.
60     if (!valueScanner.scan('%'))
61         return false;
62
63     if (number < 0 || number > 100)
64         return false;
65
66     percentage = number;
67     return true;
68 }
69
70 bool WebVTTParser::parseFloatPercentageValuePair(VTTScanner& valueScanner, char delimiter, FloatPoint& valuePair)
71 {
72     float firstCoord;
73     if (!parseFloatPercentageValue(valueScanner, firstCoord))
74         return false;
75
76     if (!valueScanner.scan(delimiter))
77         return false;
78
79     float secondCoord;
80     if (!parseFloatPercentageValue(valueScanner, secondCoord))
81         return false;
82
83     valuePair = FloatPoint(firstCoord, secondCoord);
84     return true;
85 }
86 #endif
87
88 WebVTTParser::WebVTTParser(WebVTTParserClient* client, ScriptExecutionContext* context)
89     : m_scriptExecutionContext(context)
90     , m_state(Initial)
91     , m_decoder(TextResourceDecoder::create("text/plain", UTF8Encoding()))
92     , m_currentStartTime(0)
93     , m_currentEndTime(0)
94     , m_client(client)
95 {
96 }
97
98 void WebVTTParser::getNewCues(Vector<RefPtr<WebVTTCueData>>& outputCues)
99 {
100     outputCues = m_cuelist;
101     m_cuelist.clear();
102 }
103
104 #if ENABLE(WEBVTT_REGIONS)
105 void WebVTTParser::getNewRegions(Vector<RefPtr<VTTRegion>>& outputRegions)
106 {
107     outputRegions = m_regionList;
108     m_regionList.clear();
109 }
110 #endif
111
112 void WebVTTParser::parseFileHeader(const String& data)
113 {
114     m_state = Initial;
115     m_lineReader.reset();
116     m_lineReader.append(data);
117     parse();
118 }
119
120 void WebVTTParser::parseBytes(const char* data, unsigned length)
121 {
122     String textData = m_decoder->decode(data, length);
123     m_lineReader.append(textData);
124     parse();
125 }
126
127 void WebVTTParser::parseCueData(const ISOWebVTTCue& data)
128 {
129     RefPtr<WebVTTCueData> cue = WebVTTCueData::create();
130
131     double startTime = data.presentationTime().toDouble();
132     cue->setStartTime(startTime);
133     cue->setEndTime(startTime + data.duration().toDouble());
134
135     cue->setContent(data.cueText());
136     cue->setId(data.id());
137     cue->setSettings(data.settings());
138
139     double originalStartTime;
140     if (WebVTTParser::collectTimeStamp(data.originalStartTime(), originalStartTime))
141         cue->setOriginalStartTime(originalStartTime);
142
143     m_cuelist.append(cue);
144     if (m_client)
145         m_client->newCuesParsed();
146 }
147
148 void WebVTTParser::flush()
149 {
150     String textData = m_decoder->flush();
151     m_lineReader.append(textData);
152     m_lineReader.setEndOfStream();
153     parse();
154     flushPendingCue();
155 }
156
157 void WebVTTParser::parse()
158 {    
159     // WebVTT parser algorithm. (5.1 WebVTT file parsing.)
160     // Steps 1 - 3 - Initial setup.
161     String line;
162     while (m_lineReader.getLine(line)) {
163         if (line.isNull())
164             return;
165
166         switch (m_state) {
167         case Initial:
168             // Steps 4 - 9 - Check for a valid WebVTT signature.
169             if (!hasRequiredFileIdentifier(line)) {
170                 if (m_client)
171                     m_client->fileFailedToParse();
172                 return;
173             }
174
175             m_state = Header;
176             break;
177
178         case Header:
179             collectMetadataHeader(line);
180
181             if (line.isEmpty()) {
182 #if ENABLE(WEBVTT_REGIONS)
183                 // Steps 10-14 - Allow a header (comment area) under the WEBVTT line.
184                 if (m_client && m_regionList.size())
185                     m_client->newRegionsParsed();
186 #endif
187                 m_state = Id;
188                 break;
189             }
190             // Step 15 - Break out of header loop if the line could be a timestamp line.
191             if (line.contains("-->"))
192                 m_state = recoverCue(line);
193
194             // Step 16 - Line is not the empty string and does not contain "-->".
195             break;
196
197         case Id:
198             // Steps 17 - 20 - Allow any number of line terminators, then initialize new cue values.
199             if (line.isEmpty())
200                 break;
201
202             // Step 21 - Cue creation (start a new cue).
203             resetCueValues();
204
205             // Steps 22 - 25 - Check if this line contains an optional identifier or timing data.
206             m_state = collectCueId(line);
207             break;
208
209         case TimingsAndSettings:
210             // Steps 26 - 27 - Discard current cue if the line is empty.
211             if (line.isEmpty()) {
212                 m_state = Id;
213                 break;
214             }
215
216             // Steps 28 - 29 - Collect cue timings and settings.
217             m_state = collectTimingsAndSettings(line);
218             break;
219
220         case CueText:
221             // Steps 31 - 41 - Collect the cue text, create a cue, and add it to the output.
222             m_state = collectCueText(line);
223             break;
224
225         case BadCue:
226             // Steps 42 - 48 - Discard lines until an empty line or a potential timing line is seen.
227             m_state = ignoreBadCue(line);
228             break;
229
230         case Finished:
231             ASSERT_NOT_REACHED();
232             break;
233         }
234     }
235 }
236
237 void WebVTTParser::fileFinished()
238 {
239     ASSERT(m_state != Finished);
240     parseBytes("\n\n", 2);
241     m_state = Finished;
242 }
243
244 void WebVTTParser::flushPendingCue()
245 {
246     ASSERT(m_lineReader.isAtEndOfStream());
247     // If we're in the CueText state when we run out of data, we emit the pending cue.
248     if (m_state == CueText)
249         createNewCue();
250 }
251
252 bool WebVTTParser::hasRequiredFileIdentifier(const String& line)
253 {
254     // A WebVTT file identifier consists of an optional BOM character,
255     // the string "WEBVTT" followed by an optional space or tab character,
256     // and any number of characters that are not line terminators ...
257     if (line.isEmpty())
258         return false;
259
260     if (!line.startsWith(fileIdentifier, fileIdentifierLength))
261         return false;
262
263     if (line.length() > fileIdentifierLength && !isASpace(line[fileIdentifierLength]))
264         return false;
265     return true;
266 }
267
268 void WebVTTParser::collectMetadataHeader(const String& line)
269 {
270 #if ENABLE(WEBVTT_REGIONS)
271     // WebVTT header parsing (WebVTT parser algorithm step 12)
272     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, regionHeaderName, ("Region", AtomicString::ConstructFromLiteral));
273
274     // Step 12.4 If line contains the character ":" (A U+003A COLON), then set metadata's
275     // name to the substring of line before the first ":" character and
276     // metadata's value to the substring after this character.
277     size_t colonPosition = line.find(':');
278     if (colonPosition == notFound)
279         return;
280
281     String headerName = line.substring(0, colonPosition);
282
283     // Step 12.5 If metadata's name equals "Region":
284     if (headerName == regionHeaderName) {
285         String headerValue = line.substring(colonPosition + 1, line.length() - 1);
286         // Steps 12.5.1 - 12.5.11 Region creation: Let region be a new text track region [...]
287         createNewRegion(headerValue);
288     }
289 #else
290     UNUSED_PARAM(line);
291 #endif
292 }
293
294 WebVTTParser::ParseState WebVTTParser::collectCueId(const String& line)
295 {
296     if (line.contains("-->"))
297         return collectTimingsAndSettings(line);
298     m_currentId = line;
299     return TimingsAndSettings;
300 }
301
302 WebVTTParser::ParseState WebVTTParser::collectTimingsAndSettings(const String& line)
303 {
304     if (line.isEmpty())
305         return BadCue;
306
307     VTTScanner input(line);
308
309     // Collect WebVTT cue timings and settings. (5.3 WebVTT cue timings and settings parsing.)
310     // Steps 1 - 3 - Let input be the string being parsed and position be a pointer into input
311     input.skipWhile<isASpace>();
312
313     // Steps 4 - 5 - Collect a WebVTT timestamp. If that fails, then abort and return failure. Otherwise, let cue's text track cue start time be the collected time.
314     if (!collectTimeStamp(input, m_currentStartTime))
315         return BadCue;
316     
317     input.skipWhile<isASpace>();
318
319     // Steps 6 - 9 - If the next three characters are not "-->", abort and return failure.
320     if (!input.scan("-->"))
321         return BadCue;
322     
323     input.skipWhile<isASpace>();
324
325     // Steps 10 - 11 - Collect a WebVTT timestamp. If that fails, then abort and return failure. Otherwise, let cue's text track cue end time be the collected time.
326     if (!collectTimeStamp(input, m_currentEndTime))
327         return BadCue;
328
329     input.skipWhile<isASpace>();
330
331     // Step 12 - Parse the WebVTT settings for the cue (conducted in TextTrackCue).
332     m_currentSettings = input.restOfInputAsString();
333     return CueText;
334 }
335
336 WebVTTParser::ParseState WebVTTParser::collectCueText(const String& line)
337 {
338     // Step 34.
339     if (line.isEmpty()) {
340         createNewCue();
341         return Id;
342     }
343     // Step 35.
344     if (line.contains("-->")) {
345         // Step 39-40.
346         createNewCue();
347
348         // Step 41 - New iteration of the cue loop.
349         return recoverCue(line);
350     }
351     if (!m_currentContent.isEmpty())
352         m_currentContent.append("\n");
353     m_currentContent.append(line);
354
355     return CueText;
356 }
357
358 WebVTTParser::ParseState WebVTTParser::recoverCue(const String& line)
359 {
360     // Step 17 and 21.
361     resetCueValues();
362
363     // Step 22.
364     return collectTimingsAndSettings(line);
365 }
366
367 WebVTTParser::ParseState WebVTTParser::ignoreBadCue(const String& line)
368 {
369     if (line.isEmpty())
370         return Id;
371     if (line.contains("-->"))
372         return recoverCue(line);
373     return BadCue;
374 }
375
376 // A helper class for the construction of a "cue fragment" from the cue text.
377 class WebVTTTreeBuilder {
378 public:
379     WebVTTTreeBuilder(Document& document)
380         : m_document(document) { }
381
382     PassRefPtr<DocumentFragment> buildFromString(const String& cueText);
383
384 private:
385     void constructTreeFromToken(Document&);
386
387     WebVTTToken m_token;
388     RefPtr<ContainerNode> m_currentNode;
389     Vector<AtomicString> m_languageStack;
390     Document& m_document;
391 };
392
393 PassRefPtr<DocumentFragment> WebVTTTreeBuilder::buildFromString(const String& cueText)
394 {
395     // Cue text processing based on
396     // 5.4 WebVTT cue text parsing rules, and
397     // 5.5 WebVTT cue text DOM construction rules.
398     RefPtr<DocumentFragment> fragment = DocumentFragment::create(m_document);
399
400     if (cueText.isEmpty()) {
401         fragment->parserAppendChild(Text::create(m_document, emptyString()));
402         return fragment.release();
403     }
404
405     m_currentNode = fragment;
406
407     WebVTTTokenizer tokenizer(cueText);
408     m_languageStack.clear();
409
410     while (tokenizer.nextToken(m_token))
411         constructTreeFromToken(m_document);
412     
413     return fragment.release();
414 }
415
416 PassRefPtr<DocumentFragment> WebVTTParser::createDocumentFragmentFromCueText(Document& document, const String& cueText)
417 {
418     WebVTTTreeBuilder treeBuilder(document);
419     return treeBuilder.buildFromString(cueText);
420 }
421
422 void WebVTTParser::createNewCue()
423 {
424     RefPtr<WebVTTCueData> cue = WebVTTCueData::create();
425     cue->setStartTime(m_currentStartTime);
426     cue->setEndTime(m_currentEndTime);
427     cue->setContent(m_currentContent.toString());
428     cue->setId(m_currentId);
429     cue->setSettings(m_currentSettings);
430
431     m_cuelist.append(cue);
432     if (m_client)
433         m_client->newCuesParsed();
434 }
435
436 void WebVTTParser::resetCueValues()
437 {
438     m_currentId = emptyString();
439     m_currentSettings = emptyString();
440     m_currentStartTime = 0;
441     m_currentEndTime = 0;
442     m_currentContent.clear();
443 }
444
445 #if ENABLE(WEBVTT_REGIONS)
446 void WebVTTParser::createNewRegion(const String& headerValue)
447 {
448     if (headerValue.isEmpty())
449         return;
450
451     // Steps 12.5.1 - 12.5.9 - Construct and initialize a WebVTT Region object.
452     RefPtr<VTTRegion> region = VTTRegion::create(*m_scriptExecutionContext);
453     region->setRegionSettings(headerValue);
454
455     // Step 12.5.10 If the text track list of regions regions contains a region
456     // with the same region identifier value as region, remove that region.
457     for (size_t i = 0; i < m_regionList.size(); ++i)
458         if (m_regionList[i]->id() == region->id()) {
459             m_regionList.remove(i);
460             break;
461         }
462
463     // Step 12.5.11
464     m_regionList.append(region);
465 }
466 #endif
467
468 bool WebVTTParser::collectTimeStamp(const String& line, double& timeStamp)
469 {
470     if (line.isEmpty())
471         return false;
472
473     VTTScanner input(line);
474     return collectTimeStamp(input, timeStamp);
475 }
476
477 bool WebVTTParser::collectTimeStamp(VTTScanner& input, double& timeStamp)
478 {
479     // Collect a WebVTT timestamp (5.3 WebVTT cue timings and settings parsing.)
480     // Steps 1 - 4 - Initial checks, let most significant units be minutes.
481     enum Mode { minutes, hours };
482     Mode mode = minutes;
483
484     // Steps 5 - 7 - Collect a sequence of characters that are 0-9.
485     // If not 2 characters or value is greater than 59, interpret as hours.
486     int value1;
487     unsigned value1Digits = input.scanDigits(value1);
488     if (!value1Digits)
489         return false;
490     if (value1Digits != 2 || value1 > 59)
491         mode = hours;
492
493     // Steps 8 - 11 - Collect the next sequence of 0-9 after ':' (must be 2 chars).
494     int value2;
495     if (!input.scan(':') || input.scanDigits(value2) != 2)
496         return false;
497
498     // Step 12 - Detect whether this timestamp includes hours.
499     int value3;
500     if (mode == hours || input.match(':')) {
501         if (!input.scan(':') || input.scanDigits(value3) != 2)
502             return false;
503     } else {
504         value3 = value2;
505         value2 = value1;
506         value1 = 0;
507     }
508
509     // Steps 13 - 17 - Collect next sequence of 0-9 after '.' (must be 3 chars).
510     int value4;
511     if (!input.scan('.') || input.scanDigits(value4) != 3)
512         return false;
513     if (value2 > 59 || value3 > 59)
514         return false;
515
516     // Steps 18 - 19 - Calculate result.
517     timeStamp = (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + (value4 * secondsPerMillisecond);
518     return true;
519 }
520
521 static WebVTTNodeType tokenToNodeType(WebVTTToken& token)
522 {
523     switch (token.name().length()) {
524     case 1:
525         if (token.name()[0] == 'c')
526             return WebVTTNodeTypeClass;
527         if (token.name()[0] == 'v')
528             return WebVTTNodeTypeVoice;
529         if (token.name()[0] == 'b')
530             return WebVTTNodeTypeBold;
531         if (token.name()[0] == 'i')
532             return WebVTTNodeTypeItalic;
533         if (token.name()[0] == 'u')
534             return WebVTTNodeTypeUnderline;
535         break;
536     case 2:
537         if (token.name()[0] == 'r' && token.name()[1] == 't')
538             return WebVTTNodeTypeRubyText;
539         break;
540     case 4:
541         if (token.name()[0] == 'r' && token.name()[1] == 'u' && token.name()[2] == 'b' && token.name()[3] == 'y')
542             return WebVTTNodeTypeRuby;
543         if (token.name()[0] == 'l' && token.name()[1] == 'a' && token.name()[2] == 'n' && token.name()[3] == 'g')
544             return WebVTTNodeTypeLanguage;
545         break;
546     }
547     return WebVTTNodeTypeNone;
548 }
549
550 void WebVTTTreeBuilder::constructTreeFromToken(Document& document)
551 {
552     // http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules
553
554     switch (m_token.type()) {
555     case WebVTTTokenTypes::Character: {
556         RefPtr<Text> child = Text::create(document, m_token.characters());
557         m_currentNode->parserAppendChild(child);
558         break;
559     }
560     case WebVTTTokenTypes::StartTag: {
561         WebVTTNodeType nodeType = tokenToNodeType(m_token);
562         if (nodeType == WebVTTNodeTypeNone)
563             break;
564
565         WebVTTNodeType currentType = m_currentNode->isWebVTTElement() ? toWebVTTElement(m_currentNode.get())->webVTTNodeType() : WebVTTNodeTypeNone;
566         // <rt> is only allowed if the current node is <ruby>.
567         if (nodeType == WebVTTNodeTypeRubyText && currentType != WebVTTNodeTypeRuby)
568             break;
569
570         RefPtr<WebVTTElement> child = WebVTTElement::create(nodeType, document);
571         if (!m_token.classes().isEmpty())
572             child->setAttribute(classAttr, m_token.classes());
573
574         if (nodeType == WebVTTNodeTypeVoice)
575             child->setAttribute(WebVTTElement::voiceAttributeName(), m_token.annotation());
576         else if (nodeType == WebVTTNodeTypeLanguage) {
577             m_languageStack.append(m_token.annotation());
578             child->setAttribute(WebVTTElement::langAttributeName(), m_languageStack.last());
579         }
580         if (!m_languageStack.isEmpty())
581             child->setLanguage(m_languageStack.last());
582         m_currentNode->parserAppendChild(child);
583         m_currentNode = child;
584         break;
585     }
586     case WebVTTTokenTypes::EndTag: {
587         WebVTTNodeType nodeType = tokenToNodeType(m_token);
588         if (nodeType == WebVTTNodeTypeNone)
589             break;
590         
591         // The only non-VTTElement would be the DocumentFragment root. (Text
592         // nodes and PIs will never appear as m_currentNode.)
593         if (!m_currentNode->isWebVTTElement())
594             break;
595
596         WebVTTNodeType currentType = toWebVTTElement(m_currentNode.get())->webVTTNodeType();
597         bool matchesCurrent = nodeType == currentType;
598         if (!matchesCurrent) {
599             // </ruby> auto-closes <rt>
600             if (currentType == WebVTTNodeTypeRubyText && nodeType == WebVTTNodeTypeRuby) {
601                 if (m_currentNode->parentNode())
602                     m_currentNode = m_currentNode->parentNode();
603             } else
604                 break;
605         }
606         if (nodeType == WebVTTNodeTypeLanguage)
607             m_languageStack.removeLast();
608         if (m_currentNode->parentNode())
609             m_currentNode = m_currentNode->parentNode();
610         break;
611     }
612     case WebVTTTokenTypes::TimestampTag: {
613         String charactersString = m_token.characters();
614         double parsedTimeStamp;
615         if (WebVTTParser::collectTimeStamp(charactersString, parsedTimeStamp))
616             m_currentNode->parserAppendChild(ProcessingInstruction::create(document, "timestamp", charactersString));
617         break;
618     }
619     default:
620         break;
621     }
622 }
623
624 }
625
626 #endif