[MSE] Add MediaSource extensions to AudioTrack, VideoTrack, and TextTrack.
[WebKit-https.git] / Source / WebCore / html / track / TextTrack.cpp
1 /*
2  * Copyright (C) 2011 Google Inc.  All rights reserved.
3  * Copyright (C) 2011, 2012, 2013 Apple Inc.  All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include "config.h"
33
34 #if ENABLE(VIDEO_TRACK)
35
36 #include "TextTrack.h"
37
38 #include "Event.h"
39 #include "HTMLMediaElement.h"
40 #include "SourceBuffer.h"
41 #include "TextTrackCueList.h"
42 #include "TextTrackList.h"
43 #include "TextTrackRegionList.h"
44
45 namespace WebCore {
46
47 static const int invalidTrackIndex = -1;
48
49 const AtomicString& TextTrack::subtitlesKeyword()
50 {
51     DEFINE_STATIC_LOCAL(const AtomicString, subtitles, ("subtitles", AtomicString::ConstructFromLiteral));
52     return subtitles;
53 }
54
55 const AtomicString& TextTrack::captionsKeyword()
56 {
57     DEFINE_STATIC_LOCAL(const AtomicString, captions, ("captions", AtomicString::ConstructFromLiteral));
58     return captions;
59 }
60
61 const AtomicString& TextTrack::descriptionsKeyword()
62 {
63     DEFINE_STATIC_LOCAL(const AtomicString, descriptions, ("descriptions", AtomicString::ConstructFromLiteral));
64     return descriptions;
65 }
66
67 const AtomicString& TextTrack::chaptersKeyword()
68 {
69     DEFINE_STATIC_LOCAL(const AtomicString, chapters, ("chapters", AtomicString::ConstructFromLiteral));
70     return chapters;
71 }
72
73 const AtomicString& TextTrack::metadataKeyword()
74 {
75     DEFINE_STATIC_LOCAL(const AtomicString, metadata, ("metadata", AtomicString::ConstructFromLiteral));
76     return metadata;
77 }
78     
79 const AtomicString& TextTrack::forcedKeyword()
80 {
81     DEFINE_STATIC_LOCAL(const AtomicString, forced, ("forced", AtomicString::ConstructFromLiteral));
82     return forced;
83 }
84
85 const AtomicString& TextTrack::disabledKeyword()
86 {
87     DEFINE_STATIC_LOCAL(const AtomicString, open, ("disabled", AtomicString::ConstructFromLiteral));
88     return open;
89 }
90
91 const AtomicString& TextTrack::hiddenKeyword()
92 {
93     DEFINE_STATIC_LOCAL(const AtomicString, closed, ("hidden", AtomicString::ConstructFromLiteral));
94     return closed;
95 }
96
97 const AtomicString& TextTrack::showingKeyword()
98 {
99     DEFINE_STATIC_LOCAL(const AtomicString, ended, ("showing", AtomicString::ConstructFromLiteral));
100     return ended;
101 }
102
103 TextTrack* TextTrack::captionMenuOffItem()
104 {
105     static TextTrack* off = TextTrack::create(0, 0, "off menu item", "", "", "").leakRef();
106     return off;
107 }
108
109 TextTrack* TextTrack::captionMenuAutomaticItem()
110 {
111     static TextTrack* automatic = TextTrack::create(0, 0, "automatic menu item", "", "", "").leakRef();
112     return automatic;
113 }
114
115 TextTrack::TextTrack(ScriptExecutionContext* context, TextTrackClient* client, const AtomicString& kind, const AtomicString& id, const AtomicString& label, const AtomicString& language, TextTrackType type)
116     : TrackBase(TrackBase::TextTrack, id, label, language)
117     , m_cues(0)
118     , m_scriptExecutionContext(context)
119 #if ENABLE(WEBVTT_REGIONS)
120     , m_regions(0)
121 #endif
122     , m_mode(disabledKeyword().string())
123     , m_client(client)
124     , m_trackType(type)
125     , m_readinessState(NotLoaded)
126     , m_trackIndex(invalidTrackIndex)
127     , m_renderedTrackIndex(invalidTrackIndex)
128     , m_hasBeenConfigured(false)
129 {
130     setKindInternal(kind);
131 }
132
133 TextTrack::~TextTrack()
134 {
135     if (m_cues) {
136         if (m_client)
137             m_client->textTrackRemoveCues(this, m_cues.get());
138
139         for (size_t i = 0; i < m_cues->length(); ++i)
140             m_cues->item(i)->setTrack(0);
141 #if ENABLE(WEBVTT_REGIONS)
142         for (size_t i = 0; i < m_regions->length(); ++i)
143             m_regions->item(i)->setTrack(0);
144 #endif
145     }
146     clearClient();
147 }
148
149 bool TextTrack::isValidKind(const AtomicString& value) const
150 {
151     return TextTrack::isValidKindKeyword(value);
152 }
153
154 bool TextTrack::enabled() const
155 {
156     return m_mode != disabledKeyword();
157 }
158
159 bool TextTrack::isValidKindKeyword(const AtomicString& value)
160 {
161     if (value == subtitlesKeyword())
162         return true;
163     if (value == captionsKeyword())
164         return true;
165     if (value == descriptionsKeyword())
166         return true;
167     if (value == chaptersKeyword())
168         return true;
169     if (value == metadataKeyword())
170         return true;
171     if (value == forcedKeyword())
172         return true;
173
174     return false;
175 }
176
177 void TextTrack::setKind(const AtomicString& newKind)
178 {
179     String oldKind = kind();
180
181 #if ENABLE(MEDIA_SOURCE)
182     // 10.1 kind, on setting:
183     // 1. If the value being assigned to this attribute does not match one of the text track kinds,
184     // then abort these steps.
185     if (!isValidKindKeyword(newKind))
186         return;
187
188     // 2. Update this attribute to the new value.
189     setKindInternal(newKind);
190
191     // 3. If the sourceBuffer attribute on this track is not null, then queue a task to fire a simple
192     // event named change at sourceBuffer.textTracks.
193     if (m_sourceBuffer)
194         m_sourceBuffer->textTracks()->scheduleChangeEvent();
195
196     // 4. Queue a task to fire a simple event named change at the TextTrackList object referenced by
197     // the textTracks attribute on the HTMLMediaElement.
198     if (mediaElement())
199         mediaElement()->textTracks()->scheduleChangeEvent();
200 #else
201     TrackBase::setKind(newKind);
202 #endif
203
204     if (m_client && oldKind != kind())
205         m_client->textTrackKindChanged(this);
206 }
207
208 void TextTrack::setMode(const AtomicString& mode)
209 {
210     // On setting, if the new value isn't equal to what the attribute would currently
211     // return, the new value must be processed as follows ...
212     if (mode != disabledKeyword() && mode != hiddenKeyword() && mode != showingKeyword())
213         return;
214
215     if (m_mode == mode)
216         return;
217
218     // If mode changes to disabled, remove this track's cues from the client
219     // because they will no longer be accessible from the cues() function.
220     if (mode == disabledKeyword() && m_client && m_cues)
221         m_client->textTrackRemoveCues(this, m_cues.get());
222          
223     if (mode != showingKeyword() && m_cues)
224         for (size_t i = 0; i < m_cues->length(); ++i)
225             m_cues->item(i)->removeDisplayTree();
226
227     m_mode = mode;
228
229     if (m_client)
230         m_client->textTrackModeChanged(this);
231 }
232
233 TextTrackCueList* TextTrack::cues()
234 {
235     // 4.8.10.12.5 If the text track mode ... is not the text track disabled mode,
236     // then the cues attribute must return a live TextTrackCueList object ...
237     // Otherwise, it must return null. When an object is returned, the
238     // same object must be returned each time.
239     // http://www.whatwg.org/specs/web-apps/current-work/#dom-texttrack-cues
240     if (m_mode != disabledKeyword())
241         return ensureTextTrackCueList();
242     return 0;
243 }
244
245 void TextTrack::removeAllCues()
246 {
247     if (!m_cues)
248         return;
249
250     if (m_client)
251         m_client->textTrackRemoveCues(this, m_cues.get());
252     
253     for (size_t i = 0; i < m_cues->length(); ++i)
254         m_cues->item(i)->setTrack(0);
255     
256     m_cues = 0;
257 }
258
259 TextTrackCueList* TextTrack::activeCues() const
260 {
261     // 4.8.10.12.5 If the text track mode ... is not the text track disabled mode,
262     // then the activeCues attribute must return a live TextTrackCueList object ...
263     // ... whose active flag was set when the script started, in text track cue
264     // order. Otherwise, it must return null. When an object is returned, the
265     // same object must be returned each time.
266     // http://www.whatwg.org/specs/web-apps/current-work/#dom-texttrack-activecues
267     if (m_cues && m_mode != disabledKeyword())
268         return m_cues->activeCues();
269     return 0;
270 }
271
272 void TextTrack::addCue(PassRefPtr<TextTrackCue> prpCue)
273 {
274     if (!prpCue)
275         return;
276
277     RefPtr<TextTrackCue> cue = prpCue;
278
279     // TODO(93143): Add spec-compliant behavior for negative time values.
280     if (std::isnan(cue->startTime()) || std::isnan(cue->endTime()) || cue->startTime() < 0 || cue->endTime() < 0)
281         return;
282
283     // 4.8.10.12.5 Text track API
284
285     // The addCue(cue) method of TextTrack objects, when invoked, must run the following steps:
286
287     // 1. If the given cue is in a text track list of cues, then remove cue from that text track
288     // list of cues.
289     TextTrack* cueTrack = cue->track();
290     if (cueTrack && cueTrack != this)
291         cueTrack->removeCue(cue.get(), ASSERT_NO_EXCEPTION);
292
293     // 2. Add cue to the method's TextTrack object's text track's text track list of cues.
294     cue->setTrack(this);
295     ensureTextTrackCueList()->add(cue);
296     
297     if (m_client)
298         m_client->textTrackAddCue(this, cue.get());
299 }
300
301 void TextTrack::removeCue(TextTrackCue* cue, ExceptionCode& ec)
302 {
303     if (!cue)
304         return;
305
306     // 4.8.10.12.5 Text track API
307
308     // The removeCue(cue) method of TextTrack objects, when invoked, must run the following steps:
309
310     // 1. If the given cue is not currently listed in the method's TextTrack 
311     // object's text track's text track list of cues, then throw a NotFoundError exception.
312     if (cue->track() != this) {
313         ec = NOT_FOUND_ERR;
314         return;
315     }
316
317     // 2. Remove cue from the method's TextTrack object's text track's text track list of cues.
318     if (!m_cues || !m_cues->remove(cue)) {
319         ec = INVALID_STATE_ERR;
320         return;
321     }
322
323     cue->setTrack(0);
324     if (m_client)
325         m_client->textTrackRemoveCue(this, cue);
326 }
327
328 #if ENABLE(VIDEO_TRACK) && ENABLE(WEBVTT_REGIONS)
329 TextTrackRegionList* TextTrack::regionList()
330 {
331     return ensureTextTrackRegionList();
332 }
333
334 TextTrackRegionList* TextTrack::ensureTextTrackRegionList()
335 {
336     if (!m_regions)
337         m_regions = TextTrackRegionList::create();
338
339     return m_regions.get();
340 }
341
342 TextTrackRegionList* TextTrack::regions()
343 {
344     // If the text track mode of the text track that the TextTrack object
345     // represents is not the text track disabled mode, then the regions
346     // attribute must return a live TextTrackRegionList object that represents
347     // the text track list of regions of the text track. Otherwise, it must
348     // return null. When an object is returned, the same object must be returned
349     // each time.
350     if (m_mode != disabledKeyword())
351         return ensureTextTrackRegionList();
352
353     return 0;
354 }
355
356 void TextTrack::addRegion(PassRefPtr<TextTrackRegion> prpRegion)
357 {
358     if (!prpRegion)
359         return;
360
361     RefPtr<TextTrackRegion> region = prpRegion;
362     TextTrackRegionList* regionList = ensureTextTrackRegionList();
363
364     // 1. If the given region is in a text track list of regions, then remove
365     // region from that text track list of regions.
366     TextTrack* regionTrack = region->track();
367     if (regionTrack && regionTrack != this)
368         regionTrack->removeRegion(region.get(), ASSERT_NO_EXCEPTION);
369
370     // 2. If the method's TextTrack object's text track list of regions contains
371     // a region with the same identifier as region replace the values of that
372     // region's width, height, anchor point, viewport anchor point and scroll
373     // attributes with those of region.
374     TextTrackRegion* existingRegion = regionList->getRegionById(region->id());
375     if (existingRegion) {
376         existingRegion->updateParametersFromRegion(region.get());
377         return;
378     }
379
380     // Otherwise: add region to the method's TextTrack object's text track
381     // list of regions.
382     region->setTrack(this);
383     regionList->add(region);
384 }
385
386 void TextTrack::removeRegion(TextTrackRegion* region, ExceptionCode &ec)
387 {
388     if (!region)
389         return;
390
391     // 1. If the given region is not currently listed in the method's TextTrack
392     // object's text track list of regions, then throw a NotFoundError exception.
393     if (region->track() != this) {
394         ec = NOT_FOUND_ERR;
395         return;
396     }
397
398     if (!m_regions || !m_regions->remove(region)) {
399         ec = INVALID_STATE_ERR;
400         return;
401     }
402
403     region->setTrack(0);
404 }
405 #endif
406
407 void TextTrack::cueWillChange(TextTrackCue* cue)
408 {
409     if (!m_client)
410         return;
411
412     // The cue may need to be repositioned in the media element's interval tree, may need to
413     // be re-rendered, etc, so remove it before the modification...
414     m_client->textTrackRemoveCue(this, cue);
415 }
416
417 void TextTrack::cueDidChange(TextTrackCue* cue)
418 {
419     if (!m_client)
420         return;
421
422     // Make sure the TextTrackCueList order is up-to-date.
423     ensureTextTrackCueList()->updateCueIndex(cue);
424
425     // ... and add it back again.
426     m_client->textTrackAddCue(this, cue);
427 }
428
429 int TextTrack::trackIndex()
430 {
431     ASSERT(m_mediaElement);
432
433     if (m_trackIndex == invalidTrackIndex)
434         m_trackIndex = m_mediaElement->textTracks()->getTrackIndex(this);
435
436     return m_trackIndex;
437 }
438
439 void TextTrack::invalidateTrackIndex()
440 {
441     m_trackIndex = invalidTrackIndex;
442     m_renderedTrackIndex = invalidTrackIndex;
443 }
444
445 bool TextTrack::isRendered()
446 {
447     if (kind() != captionsKeyword() && kind() != subtitlesKeyword() && kind() != forcedKeyword())
448         return false;
449
450     if (m_mode != showingKeyword())
451         return false;
452
453     return true;
454 }
455
456 TextTrackCueList* TextTrack::ensureTextTrackCueList()
457 {
458     if (!m_cues)
459         m_cues = TextTrackCueList::create();
460
461     return m_cues.get();
462 }
463
464 int TextTrack::trackIndexRelativeToRenderedTracks()
465 {
466     ASSERT(m_mediaElement);
467     
468     if (m_renderedTrackIndex == invalidTrackIndex)
469         m_renderedTrackIndex = m_mediaElement->textTracks()->getTrackIndexRelativeToRenderedTracks(this);
470     
471     return m_renderedTrackIndex;
472 }
473
474 bool TextTrack::hasCue(TextTrackCue* cue, TextTrackCue::CueMatchRules match)
475 {
476     if (cue->startTime() < 0 || cue->endTime() < 0)
477         return false;
478     
479     if (!m_cues || !m_cues->length())
480         return false;
481     
482     size_t searchStart = 0;
483     size_t searchEnd = m_cues->length();
484     
485     while (1) {
486         ASSERT(searchStart <= m_cues->length());
487         ASSERT(searchEnd <= m_cues->length());
488         
489         TextTrackCue* existingCue;
490         
491         // Cues in the TextTrackCueList are maintained in start time order.
492         if (searchStart == searchEnd) {
493             if (!searchStart)
494                 return false;
495
496             // If there is more than one cue with the same start time, back up to first one so we
497             // consider all of them.
498             while (searchStart >= 2 && cue->startTime() == m_cues->item(searchStart - 2)->startTime())
499                 --searchStart;
500             
501             bool firstCompare = true;
502             while (1) {
503                 if (!firstCompare)
504                     ++searchStart;
505                 firstCompare = false;
506                 if (searchStart > m_cues->length())
507                     return false;
508
509                 existingCue = m_cues->item(searchStart - 1);
510                 if (!existingCue || cue->startTime() > existingCue->startTime())
511                     return false;
512
513                 if (!existingCue->isEqual(*cue, match))
514                     continue;
515                 
516                 return true;
517             }
518         }
519         
520         size_t index = (searchStart + searchEnd) / 2;
521         existingCue = m_cues->item(index);
522         if (cue->startTime() < existingCue->startTime() || (match != TextTrackCue::IgnoreDuration && cue->startTime() == existingCue->startTime() && cue->endTime() > existingCue->endTime()))
523             searchEnd = index;
524         else
525             searchStart = index + 1;
526     }
527     
528     ASSERT_NOT_REACHED();
529     return false;
530 }
531
532 #if USE(PLATFORM_TEXT_TRACK_MENU)
533 PassRefPtr<PlatformTextTrack> TextTrack::platformTextTrack()
534 {
535     static int uniqueId = 0;
536
537     if (m_platformTextTrack)
538         return m_platformTextTrack;
539
540     PlatformTextTrack::TrackKind platformKind = PlatformTextTrack::Caption;
541     if (kind() == subtitlesKeyword())
542         platformKind = PlatformTextTrack::Subtitle;
543     else if (kind() == captionsKeyword())
544         platformKind = PlatformTextTrack::Caption;
545     else if (kind() == descriptionsKeyword())
546         platformKind = PlatformTextTrack::Description;
547     else if (kind() == chaptersKeyword())
548         platformKind = PlatformTextTrack::Chapter;
549     else if (kind() == metadataKeyword())
550         platformKind = PlatformTextTrack::MetaData;
551     else if (kind() == forcedKeyword())
552         platformKind = PlatformTextTrack::Forced;
553
554     PlatformTextTrack::TrackType type = PlatformTextTrack::OutOfBand;
555     if (m_trackType == TrackElement)
556         type = PlatformTextTrack::OutOfBand;
557     else if (m_trackType == AddTrack)
558         type = PlatformTextTrack::Script;
559     else if (m_trackType == InBand)
560         type = PlatformTextTrack::InBand;
561
562     m_platformTextTrack = PlatformTextTrack::create(this, label(), language(), platformKind, type, ++uniqueId);
563
564     return m_platformTextTrack;
565 }
566 #endif
567
568 bool TextTrack::isMainProgramContent() const
569 {
570     // "Main program" content is intrinsic to the presentation of the media file, regardless of locale. Content such as
571     // directors commentary is not "main program" because it is not essential for the presentation. HTML5 doesn't have
572     // a way to express this in a machine-reable form, it is typically done with the track label, so we assume that caption
573     // tracks are main content and all other track types are not.
574     return kind() == captionsKeyword();
575 }
576
577 #if ENABLE(MEDIA_SOURCE)
578 void TextTrack::setLanguage(const AtomicString& language)
579 {
580     // 11.1 language, on setting:
581     // 1. If the value being assigned to this attribute is not an empty string or a BCP 47 language
582     // tag[BCP47], then abort these steps.
583     // FIXME(123926): Validate the BCP47-ness of langague.
584
585     // 2. Update this attribute to the new value.
586     TrackBase::setLanguage(language);
587
588     // 3. If the sourceBuffer attribute on this track is not null, then queue a task to fire a simple
589     // event named change at sourceBuffer.textTracks.
590     if (m_sourceBuffer)
591         m_sourceBuffer->textTracks()->scheduleChangeEvent();
592
593     // 4. Queue a task to fire a simple event named change at the TextTrackList object referenced by
594     // the textTracks attribute on the HTMLMediaElement.
595     if (mediaElement())
596         mediaElement()->textTracks()->scheduleChangeEvent();
597 }
598 #endif
599
600 } // namespace WebCore
601
602 #endif