1/*
2 * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "CaptionUserPreferences.h"
28
29#if ENABLE(VIDEO_TRACK)
30
31#include "AudioTrackList.h"
32#include "DOMWrapperWorld.h"
33#include "LocalizedStrings.h"
34#include "MediaSelectionOption.h"
35#include "Page.h"
36#include "PageGroup.h"
37#include "Settings.h"
38#include "TextTrackList.h"
39#include "UserContentController.h"
40#include "UserContentTypes.h"
41#include "UserStyleSheet.h"
42#include "UserStyleSheetTypes.h"
43#include <JavaScriptCore/HeapInlines.h>
44#include <JavaScriptCore/JSCellInlines.h>
45#include <JavaScriptCore/StructureInlines.h>
46#include <wtf/Language.h>
47
48namespace WebCore {
49
50CaptionUserPreferences::CaptionUserPreferences(PageGroup& group)
51 : m_pageGroup(group)
52 , m_displayMode(ForcedOnly)
53 , m_timer(*this, &CaptionUserPreferences::timerFired)
54{
55}
56
57CaptionUserPreferences::~CaptionUserPreferences() = default;
58
59void CaptionUserPreferences::timerFired()
60{
61 captionPreferencesChanged();
62}
63
64void CaptionUserPreferences::beginBlockingNotifications()
65{
66 ++m_blockNotificationsCounter;
67}
68
69void CaptionUserPreferences::endBlockingNotifications()
70{
71 ASSERT(m_blockNotificationsCounter);
72 --m_blockNotificationsCounter;
73}
74
75void CaptionUserPreferences::notify()
76{
77 if (m_blockNotificationsCounter)
78 return;
79
80 m_havePreferences = true;
81 if (!m_timer.isActive())
82 m_timer.startOneShot(0_s);
83}
84
85CaptionUserPreferences::CaptionDisplayMode CaptionUserPreferences::captionDisplayMode() const
86{
87 return m_displayMode;
88}
89
90void CaptionUserPreferences::setCaptionDisplayMode(CaptionUserPreferences::CaptionDisplayMode mode)
91{
92 m_displayMode = mode;
93 if (m_testingMode && mode != AlwaysOn) {
94 setUserPrefersCaptions(false);
95 setUserPrefersSubtitles(false);
96 }
97 notify();
98}
99
100Page* CaptionUserPreferences::currentPage() const
101{
102 if (m_pageGroup.pages().isEmpty())
103 return nullptr;
104
105 return *(m_pageGroup.pages().begin());
106}
107
108bool CaptionUserPreferences::userPrefersCaptions() const
109{
110 Page* page = currentPage();
111 if (!page)
112 return false;
113
114 return page->settings().shouldDisplayCaptions();
115}
116
117void CaptionUserPreferences::setUserPrefersCaptions(bool preference)
118{
119 Page* page = currentPage();
120 if (!page)
121 return;
122
123 page->settings().setShouldDisplayCaptions(preference);
124 notify();
125}
126
127bool CaptionUserPreferences::userPrefersSubtitles() const
128{
129 Page* page = currentPage();
130 if (!page)
131 return false;
132
133 return page->settings().shouldDisplaySubtitles();
134}
135
136void CaptionUserPreferences::setUserPrefersSubtitles(bool preference)
137{
138 Page* page = currentPage();
139 if (!page)
140 return;
141
142 page->settings().setShouldDisplaySubtitles(preference);
143 notify();
144}
145
146bool CaptionUserPreferences::userPrefersTextDescriptions() const
147{
148 Page* page = currentPage();
149 if (!page)
150 return false;
151
152 return page->settings().shouldDisplayTextDescriptions();
153}
154
155void CaptionUserPreferences::setUserPrefersTextDescriptions(bool preference)
156{
157 Page* page = currentPage();
158 if (!page)
159 return;
160
161 page->settings().setShouldDisplayTextDescriptions(preference);
162 notify();
163}
164
165void CaptionUserPreferences::captionPreferencesChanged()
166{
167 m_pageGroup.captionPreferencesChanged();
168}
169
170Vector<String> CaptionUserPreferences::preferredLanguages() const
171{
172 Vector<String> languages = userPreferredLanguages();
173 if (m_testingMode && !m_userPreferredLanguage.isEmpty())
174 languages.insert(0, m_userPreferredLanguage);
175
176 return languages;
177}
178
179void CaptionUserPreferences::setPreferredLanguage(const String& language)
180{
181 m_userPreferredLanguage = language;
182 notify();
183}
184
185void CaptionUserPreferences::setPreferredAudioCharacteristic(const String& characteristic)
186{
187 m_userPreferredAudioCharacteristic = characteristic;
188 notify();
189}
190
191Vector<String> CaptionUserPreferences::preferredAudioCharacteristics() const
192{
193 Vector<String> characteristics;
194 if (!m_userPreferredAudioCharacteristic.isEmpty())
195 characteristics.append(m_userPreferredAudioCharacteristic);
196 return characteristics;
197}
198
199static String trackDisplayName(TextTrack* track)
200{
201 if (track == TextTrack::captionMenuOffItem())
202 return textTrackOffMenuItemText();
203 if (track == TextTrack::captionMenuAutomaticItem())
204 return textTrackAutomaticMenuItemText();
205
206 if (track->label().isEmpty() && track->validBCP47Language().isEmpty())
207 return textTrackNoLabelText();
208 if (!track->label().isEmpty())
209 return track->label();
210 return track->validBCP47Language();
211}
212
213String CaptionUserPreferences::displayNameForTrack(TextTrack* track) const
214{
215 return trackDisplayName(track);
216}
217
218MediaSelectionOption CaptionUserPreferences::mediaSelectionOptionForTrack(TextTrack* track) const
219{
220 auto type = MediaSelectionOption::Type::Regular;
221 if (track == TextTrack::captionMenuOffItem())
222 type = MediaSelectionOption::Type::LegibleOff;
223 else if (track == TextTrack::captionMenuAutomaticItem())
224 type = MediaSelectionOption::Type::LegibleAuto;
225 return { displayNameForTrack(track), type };
226}
227
228Vector<RefPtr<TextTrack>> CaptionUserPreferences::sortedTrackListForMenu(TextTrackList* trackList)
229{
230 ASSERT(trackList);
231
232 Vector<RefPtr<TextTrack>> tracksForMenu;
233
234 for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
235 TextTrack* track = trackList->item(i);
236 auto kind = track->kind();
237 if (kind == TextTrack::Kind::Captions || kind == TextTrack::Kind::Descriptions || kind == TextTrack::Kind::Subtitles)
238 tracksForMenu.append(track);
239 }
240
241 std::sort(tracksForMenu.begin(), tracksForMenu.end(), [](auto& a, auto& b) {
242 return codePointCompare(trackDisplayName(a.get()), trackDisplayName(b.get())) < 0;
243 });
244
245 tracksForMenu.insert(0, TextTrack::captionMenuOffItem());
246 tracksForMenu.insert(1, TextTrack::captionMenuAutomaticItem());
247
248 return tracksForMenu;
249}
250
251static String trackDisplayName(AudioTrack* track)
252{
253 if (track->label().isEmpty() && track->validBCP47Language().isEmpty())
254 return audioTrackNoLabelText();
255 if (!track->label().isEmpty())
256 return track->label();
257 return track->validBCP47Language();
258}
259
260String CaptionUserPreferences::displayNameForTrack(AudioTrack* track) const
261{
262 return trackDisplayName(track);
263}
264
265MediaSelectionOption CaptionUserPreferences::mediaSelectionOptionForTrack(AudioTrack* track) const
266{
267 return { displayNameForTrack(track), MediaSelectionOption::Type::Regular };
268}
269
270Vector<RefPtr<AudioTrack>> CaptionUserPreferences::sortedTrackListForMenu(AudioTrackList* trackList)
271{
272 ASSERT(trackList);
273
274 Vector<RefPtr<AudioTrack>> tracksForMenu;
275
276 for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
277 AudioTrack* track = trackList->item(i);
278 tracksForMenu.append(track);
279 }
280
281 std::sort(tracksForMenu.begin(), tracksForMenu.end(), [](auto& a, auto& b) {
282 return codePointCompare(trackDisplayName(a.get()), trackDisplayName(b.get())) < 0;
283 });
284
285 return tracksForMenu;
286}
287
288int CaptionUserPreferences::textTrackSelectionScore(TextTrack* track, HTMLMediaElement*) const
289{
290 if (track->kind() != TextTrack::Kind::Captions && track->kind() != TextTrack::Kind::Subtitles)
291 return 0;
292
293 if (!userPrefersSubtitles() && !userPrefersCaptions())
294 return 0;
295
296 return textTrackLanguageSelectionScore(track, preferredLanguages()) + 1;
297}
298
299int CaptionUserPreferences::textTrackLanguageSelectionScore(TextTrack* track, const Vector<String>& preferredLanguages) const
300{
301 if (track->validBCP47Language().isEmpty())
302 return 0;
303
304 bool exactMatch;
305 size_t languageMatchIndex = indexOfBestMatchingLanguageInList(track->validBCP47Language(), preferredLanguages, exactMatch);
306 if (languageMatchIndex >= preferredLanguages.size())
307 return 0;
308
309 // Matching a track language is more important than matching track type, so this multiplier must be
310 // greater than the maximum value returned by textTrackSelectionScore.
311 int bonus = exactMatch ? 1 : 0;
312 return (preferredLanguages.size() + bonus - languageMatchIndex) * 10;
313}
314
315void CaptionUserPreferences::setCaptionsStyleSheetOverride(const String& override)
316{
317 if (override == m_captionsStyleSheetOverride)
318 return;
319
320 m_captionsStyleSheetOverride = override;
321 updateCaptionStyleSheetOverride();
322 if (!m_timer.isActive())
323 m_timer.startOneShot(0_s);
324}
325
326void CaptionUserPreferences::updateCaptionStyleSheetOverride()
327{
328 String captionsOverrideStyleSheet = captionsStyleSheetOverride();
329 for (auto& page : m_pageGroup.pages())
330 page->setCaptionUserPreferencesStyleSheet(captionsOverrideStyleSheet);
331}
332
333String CaptionUserPreferences::primaryAudioTrackLanguageOverride() const
334{
335 if (!m_primaryAudioTrackLanguageOverride.isEmpty())
336 return m_primaryAudioTrackLanguageOverride;
337 return defaultLanguage();
338}
339
340}
341
342#endif // ENABLE(VIDEO_TRACK)
343