704b50e839edc85006bbd9d328504d6465fc5490
[WebKit-https.git] / Source / WebCore / page / CaptionUserPreferencesMediaAF.cpp
1 /*
2  * Copyright (C) 2012-2015 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
28 #if ENABLE(VIDEO_TRACK)
29
30 #if !USE(DIRECT2D)
31
32 #include "CaptionUserPreferencesMediaAF.h"
33
34 #include "AudioTrackList.h"
35 #if PLATFORM(WIN)
36 #include "CoreTextSPIWin.h"
37 #endif
38 #include "FloatConversion.h"
39 #include "HTMLMediaElement.h"
40 #include "URL.h"
41 #include "Language.h"
42 #include "LocalizedStrings.h"
43 #include "Logging.h"
44 #include "MediaControlElements.h"
45 #include "SoftLinking.h"
46 #include "TextTrackList.h"
47 #include "UserStyleSheetTypes.h"
48 #include "VTTCue.h"
49 #include <algorithm>
50 #include <wtf/NeverDestroyed.h>
51 #include <wtf/PlatformUserPreferredLanguages.h>
52 #include <wtf/RetainPtr.h>
53 #include <wtf/text/CString.h>
54 #include <wtf/text/StringBuilder.h>
55
56 #if PLATFORM(IOS)
57 #import "WebCoreThreadRun.h"
58 #endif
59
60 #if COMPILER(MSVC)
61 // See https://msdn.microsoft.com/en-us/library/35bhkfb6.aspx
62 #pragma warning(disable: 4273)
63 #endif
64
65 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
66 #include <CoreText/CoreText.h>
67 #include <MediaAccessibility/MediaAccessibility.h>
68
69 #include "MediaAccessibilitySoftLink.h"
70
71 #if PLATFORM(WIN)
72
73 #ifdef DEBUG_ALL
74 #define SOFT_LINK_AVF_FRAMEWORK(Lib) SOFT_LINK_DEBUG_LIBRARY(Lib)
75 #else
76 #define SOFT_LINK_AVF_FRAMEWORK(Lib) SOFT_LINK_LIBRARY(Lib)
77 #endif
78
79 #define SOFT_LINK_AVF(Lib, Name, Type) SOFT_LINK_DLL_IMPORT(Lib, Name, Type)
80 #define SOFT_LINK_AVF_POINTER(Lib, Name, Type) SOFT_LINK_VARIABLE_DLL_IMPORT_OPTIONAL(Lib, Name, Type)
81 #define SOFT_LINK_AVF_FRAMEWORK_IMPORT(Lib, Fun, ReturnType, Arguments, Signature) SOFT_LINK_DLL_IMPORT(Lib, Fun, ReturnType, __cdecl, Arguments, Signature)
82 #define SOFT_LINK_AVF_FRAMEWORK_IMPORT_OPTIONAL(Lib, Fun, ReturnType, Arguments) SOFT_LINK_DLL_IMPORT_OPTIONAL(Lib, Fun, ReturnType, __cdecl, Arguments)
83
84 SOFT_LINK_AVF_FRAMEWORK(CoreMedia)
85 SOFT_LINK_AVF_FRAMEWORK_IMPORT_OPTIONAL(CoreMedia, MTEnableCaption2015Behavior, Boolean, ())
86
87 #else
88
89 SOFT_LINK_FRAMEWORK(MediaToolbox)
90 SOFT_LINK_OPTIONAL(MediaToolbox, MTEnableCaption2015Behavior, Boolean, (), ())
91
92 #endif // PLATFORM(WIN)
93
94 #endif // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
95
96 namespace WebCore {
97
98 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
99 static void userCaptionPreferencesChangedNotificationCallback(CFNotificationCenterRef, void* observer, CFStringRef, const void *, CFDictionaryRef)
100 {
101 #if !PLATFORM(IOS)
102     static_cast<CaptionUserPreferencesMediaAF*>(observer)->captionPreferencesChanged();
103 #else
104     WebThreadRun(^{
105         static_cast<CaptionUserPreferencesMediaAF*>(observer)->captionPreferencesChanged();
106     });
107 #endif
108 }
109 #endif
110
111 CaptionUserPreferencesMediaAF::CaptionUserPreferencesMediaAF(PageGroup& group)
112     : CaptionUserPreferences(group)
113 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
114     , m_updateStyleSheetTimer(*this, &CaptionUserPreferencesMediaAF::updateTimerFired)
115     , m_listeningForPreferenceChanges(false)
116 #endif
117 {
118     static bool initialized;
119     if (!initialized) {
120         initialized = true;
121
122         MTEnableCaption2015BehaviorPtrType function = MTEnableCaption2015BehaviorPtr();
123         if (!function || !function())
124             return;
125
126         beginBlockingNotifications();
127         CaptionUserPreferences::setCaptionDisplayMode(Manual);
128         setUserPrefersCaptions(false);
129         setUserPrefersSubtitles(false);
130         setUserPrefersTextDescriptions(false);
131         endBlockingNotifications();
132     }
133 }
134
135 CaptionUserPreferencesMediaAF::~CaptionUserPreferencesMediaAF()
136 {
137 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
138     if (kMAXCaptionAppearanceSettingsChangedNotification)
139         CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), this, kMAXCaptionAppearanceSettingsChangedNotification, 0);
140     if (kMAAudibleMediaSettingsChangedNotification)
141         CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), this, kMAAudibleMediaSettingsChangedNotification, 0);
142 #endif
143 }
144
145 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
146
147 CaptionUserPreferences::CaptionDisplayMode CaptionUserPreferencesMediaAF::captionDisplayMode() const
148 {
149     CaptionDisplayMode internalMode = CaptionUserPreferences::captionDisplayMode();
150     if (internalMode == Manual || testingMode() || !MediaAccessibilityLibrary())
151         return internalMode;
152
153     MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser);
154     switch (displayType) {
155     case kMACaptionAppearanceDisplayTypeForcedOnly:
156         return ForcedOnly;
157
158     case kMACaptionAppearanceDisplayTypeAutomatic:
159         return Automatic;
160
161     case kMACaptionAppearanceDisplayTypeAlwaysOn:
162         return AlwaysOn;
163     }
164
165     ASSERT_NOT_REACHED();
166     return ForcedOnly;
167 }
168     
169 void CaptionUserPreferencesMediaAF::setCaptionDisplayMode(CaptionUserPreferences::CaptionDisplayMode mode)
170 {
171     if (testingMode() || !MediaAccessibilityLibrary()) {
172         CaptionUserPreferences::setCaptionDisplayMode(mode);
173         return;
174     }
175
176     if (captionDisplayMode() == Manual)
177         return;
178
179     MACaptionAppearanceDisplayType displayType = kMACaptionAppearanceDisplayTypeForcedOnly;
180     switch (mode) {
181     case Automatic:
182         displayType = kMACaptionAppearanceDisplayTypeAutomatic;
183         break;
184     case ForcedOnly:
185         displayType = kMACaptionAppearanceDisplayTypeForcedOnly;
186         break;
187     case AlwaysOn:
188         displayType = kMACaptionAppearanceDisplayTypeAlwaysOn;
189         break;
190     default:
191         ASSERT_NOT_REACHED();
192         break;
193     }
194
195     MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, displayType);
196 }
197
198 bool CaptionUserPreferencesMediaAF::userPrefersCaptions() const
199 {
200     bool captionSetting = CaptionUserPreferences::userPrefersCaptions();
201     if (captionSetting || testingMode() || !MediaAccessibilityLibrary())
202         return captionSetting;
203     
204     RetainPtr<CFArrayRef> captioningMediaCharacteristics = adoptCF(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser));
205     return captioningMediaCharacteristics && CFArrayGetCount(captioningMediaCharacteristics.get());
206 }
207
208 bool CaptionUserPreferencesMediaAF::userPrefersSubtitles() const
209 {
210     bool subtitlesSetting = CaptionUserPreferences::userPrefersSubtitles();
211     if (subtitlesSetting || testingMode() || !MediaAccessibilityLibrary())
212         return subtitlesSetting;
213     
214     RetainPtr<CFArrayRef> captioningMediaCharacteristics = adoptCF(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser));
215     return !(captioningMediaCharacteristics && CFArrayGetCount(captioningMediaCharacteristics.get()));
216 }
217
218 void CaptionUserPreferencesMediaAF::updateTimerFired()
219 {
220     updateCaptionStyleSheetOverride();
221 }
222
223 void CaptionUserPreferencesMediaAF::setInterestedInCaptionPreferenceChanges()
224 {
225     if (m_listeningForPreferenceChanges)
226         return;
227
228     if (!MediaAccessibilityLibrary())
229         return;
230
231     if (!kMAXCaptionAppearanceSettingsChangedNotification && !canLoad_MediaAccessibility_kMAAudibleMediaSettingsChangedNotification())
232         return;
233
234     m_listeningForPreferenceChanges = true;
235     m_registeringForNotification = true;
236
237     if (kMAXCaptionAppearanceSettingsChangedNotification)
238         CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), this, userCaptionPreferencesChangedNotificationCallback, kMAXCaptionAppearanceSettingsChangedNotification, 0, CFNotificationSuspensionBehaviorCoalesce);
239     if (canLoad_MediaAccessibility_kMAAudibleMediaSettingsChangedNotification())
240         CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), this, userCaptionPreferencesChangedNotificationCallback, kMAAudibleMediaSettingsChangedNotification, 0, CFNotificationSuspensionBehaviorCoalesce);
241     m_registeringForNotification = false;
242
243     // Generating and registering the caption stylesheet can be expensive and this method is called indirectly when the parser creates an audio or
244     // video element, so do it after a brief pause.
245     m_updateStyleSheetTimer.startOneShot(0_s);
246 }
247
248 void CaptionUserPreferencesMediaAF::captionPreferencesChanged()
249 {
250     if (m_registeringForNotification)
251         return;
252
253     if (m_listeningForPreferenceChanges)
254         updateCaptionStyleSheetOverride();
255
256     CaptionUserPreferences::captionPreferencesChanged();
257 }
258
259 String CaptionUserPreferencesMediaAF::captionsWindowCSS() const
260 {
261     MACaptionAppearanceBehavior behavior;
262     RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyWindowColor(kMACaptionAppearanceDomainUser, &behavior));
263
264     Color windowColor(color.get());
265     if (!windowColor.isValid())
266         windowColor = Color::transparent;
267
268     bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
269     CGFloat opacity = MACaptionAppearanceGetWindowOpacity(kMACaptionAppearanceDomainUser, &behavior);
270     if (!important)
271         important = behavior == kMACaptionAppearanceBehaviorUseValue;
272     String windowStyle = colorPropertyCSS(CSSPropertyBackgroundColor, Color(windowColor.red(), windowColor.green(), windowColor.blue(), static_cast<int>(opacity * 255)), important);
273
274     if (!opacity)
275         return windowStyle;
276
277     return makeString(windowStyle, getPropertyNameString(CSSPropertyPadding), ": .4em !important;");
278 }
279
280 String CaptionUserPreferencesMediaAF::captionsBackgroundCSS() const
281 {
282     // This default value must be the same as the one specified in mediaControls.css for -webkit-media-text-track-past-nodes
283     // and webkit-media-text-track-future-nodes.
284     static NeverDestroyed<Color> defaultBackgroundColor(0, 0, 0, 0.8 * 255);
285
286     MACaptionAppearanceBehavior behavior;
287
288     RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyBackgroundColor(kMACaptionAppearanceDomainUser, &behavior));
289     Color backgroundColor(color.get());
290     if (!backgroundColor.isValid())
291         backgroundColor = defaultBackgroundColor;
292
293     bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
294     CGFloat opacity = MACaptionAppearanceGetBackgroundOpacity(kMACaptionAppearanceDomainUser, &behavior);
295     if (!important)
296         important = behavior == kMACaptionAppearanceBehaviorUseValue;
297     return colorPropertyCSS(CSSPropertyBackgroundColor, Color(backgroundColor.red(), backgroundColor.green(), backgroundColor.blue(), static_cast<int>(opacity * 255)), important);
298 }
299
300 Color CaptionUserPreferencesMediaAF::captionsTextColor(bool& important) const
301 {
302     MACaptionAppearanceBehavior behavior;
303     RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyForegroundColor(kMACaptionAppearanceDomainUser, &behavior));
304     Color textColor(color.get());
305     if (!textColor.isValid())
306         // This default value must be the same as the one specified in mediaControls.css for -webkit-media-text-track-container.
307         textColor = Color::white;
308     
309     important = behavior == kMACaptionAppearanceBehaviorUseValue;
310     CGFloat opacity = MACaptionAppearanceGetForegroundOpacity(kMACaptionAppearanceDomainUser, &behavior);
311     if (!important)
312         important = behavior == kMACaptionAppearanceBehaviorUseValue;
313     return Color(textColor.red(), textColor.green(), textColor.blue(), static_cast<int>(opacity * 255));
314 }
315     
316 String CaptionUserPreferencesMediaAF::captionsTextColorCSS() const
317 {
318     bool important;
319     Color textColor = captionsTextColor(important);
320
321     if (!textColor.isValid())
322         return emptyString();
323
324     return colorPropertyCSS(CSSPropertyColor, textColor, important);
325 }
326
327 static void appendCSS(StringBuilder& builder, CSSPropertyID id, const String& value, bool important)
328 {
329     builder.append(getPropertyNameString(id));
330     builder.append(':');
331     builder.append(value);
332     if (important)
333         builder.appendLiteral(" !important");
334     builder.append(';');
335 }
336     
337 String CaptionUserPreferencesMediaAF::windowRoundedCornerRadiusCSS() const
338 {
339     MACaptionAppearanceBehavior behavior;
340     CGFloat radius = MACaptionAppearanceGetWindowRoundedCornerRadius(kMACaptionAppearanceDomainUser, &behavior);
341     if (!radius)
342         return emptyString();
343
344     StringBuilder builder;
345     appendCSS(builder, CSSPropertyBorderRadius, String::format("%.02fpx", radius), behavior == kMACaptionAppearanceBehaviorUseValue);
346     return builder.toString();
347 }
348
349 String CaptionUserPreferencesMediaAF::colorPropertyCSS(CSSPropertyID id, const Color& color, bool important) const
350 {
351     StringBuilder builder;
352     appendCSS(builder, id, color.serialized(), important);
353     return builder.toString();
354 }
355
356 bool CaptionUserPreferencesMediaAF::captionStrokeWidthForFont(float fontSize, const String& language, float& strokeWidth, bool& important) const
357 {
358     if (!canLoad_MediaAccessibility_MACaptionAppearanceCopyFontDescriptorWithStrokeForStyle())
359         return false;
360     
361     MACaptionAppearanceBehavior behavior;
362     auto trackLanguage = language.createCFString();
363     CGFloat strokeWidthPt;
364     
365     auto fontDescriptor = adoptCF(MACaptionAppearanceCopyFontDescriptorWithStrokeForStyle(kMACaptionAppearanceDomainUser, &behavior, kMACaptionAppearanceFontStyleDefault, trackLanguage.get(), fontSize, &strokeWidthPt));
366
367     if (!fontDescriptor)
368         return false;
369
370     strokeWidth = strokeWidthPt;
371     important = behavior == kMACaptionAppearanceBehaviorUseValue;
372     
373     // Currently, MACaptionAppearanceCopyFontDescriptorWithStrokeForStyle is returning very large stroke widths, see <rdar://problem/31126629>.
374     // To avoid stroke widths that are too large, we set a maximum value of 10% of the font size.
375     strokeWidth = std::min(strokeWidth, fontSize / 10.0f);
376     
377     return true;
378 }
379
380 String CaptionUserPreferencesMediaAF::captionsTextEdgeCSS() const
381 {
382     static NeverDestroyed<const String> edgeStyleRaised(ASCIILiteral(" -.1em -.1em .16em "));
383     static NeverDestroyed<const String> edgeStyleDepressed(ASCIILiteral(" .1em .1em .16em "));
384     static NeverDestroyed<const String> edgeStyleDropShadow(ASCIILiteral(" 0 .1em .16em "));
385
386     MACaptionAppearanceBehavior behavior;
387     MACaptionAppearanceTextEdgeStyle textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(kMACaptionAppearanceDomainUser, &behavior);
388     
389     if (textEdgeStyle == kMACaptionAppearanceTextEdgeStyleUndefined || textEdgeStyle == kMACaptionAppearanceTextEdgeStyleNone)
390         return emptyString();
391
392     StringBuilder builder;
393     bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
394     if (textEdgeStyle == kMACaptionAppearanceTextEdgeStyleRaised)
395         appendCSS(builder, CSSPropertyTextShadow, makeString(edgeStyleRaised.get(), " black"), important);
396     else if (textEdgeStyle == kMACaptionAppearanceTextEdgeStyleDepressed)
397         appendCSS(builder, CSSPropertyTextShadow, makeString(edgeStyleDepressed.get(), " black"), important);
398     else if (textEdgeStyle == kMACaptionAppearanceTextEdgeStyleDropShadow)
399         appendCSS(builder, CSSPropertyTextShadow, makeString(edgeStyleDropShadow.get(), " black"), important);
400
401     if (textEdgeStyle == kMACaptionAppearanceTextEdgeStyleDropShadow || textEdgeStyle == kMACaptionAppearanceTextEdgeStyleUniform) {
402         appendCSS(builder, CSSPropertyWebkitTextStrokeColor, "black", important);
403         appendCSS(builder, CSSPropertyPaintOrder, getValueName(CSSValueStroke), important);
404         appendCSS(builder, CSSPropertyStrokeLinejoin, getValueName(CSSValueRound), important);
405         appendCSS(builder, CSSPropertyStrokeLinecap, getValueName(CSSValueRound), important);
406     }
407     
408     return builder.toString();
409 }
410
411 String CaptionUserPreferencesMediaAF::captionsDefaultFontCSS() const
412 {
413     MACaptionAppearanceBehavior behavior;
414     
415     RetainPtr<CTFontDescriptorRef> font = adoptCF(MACaptionAppearanceCopyFontDescriptorForStyle(kMACaptionAppearanceDomainUser, &behavior, kMACaptionAppearanceFontStyleDefault));
416     if (!font)
417         return emptyString();
418
419     RetainPtr<CFTypeRef> name = adoptCF(CTFontDescriptorCopyAttribute(font.get(), kCTFontNameAttribute));
420     if (!name)
421         return emptyString();
422
423     StringBuilder builder;
424     
425     builder.append(getPropertyNameString(CSSPropertyFontFamily));
426     builder.appendLiteral(": \"");
427     builder.append(static_cast<CFStringRef>(name.get()));
428     builder.append('"');
429
430     auto cascadeList = adoptCF(static_cast<CFArrayRef>(CTFontDescriptorCopyAttribute(font.get(), kCTFontCascadeListAttribute)));
431
432     if (cascadeList) {
433         for (CFIndex i = 0; i < CFArrayGetCount(cascadeList.get()); i++) {
434             auto fontCascade = static_cast<CTFontDescriptorRef>(CFArrayGetValueAtIndex(cascadeList.get(), i));
435             if (!fontCascade)
436                 continue;
437             auto fontCascadeName = adoptCF(CTFontDescriptorCopyAttribute(fontCascade, kCTFontNameAttribute));
438             if (!fontCascadeName)
439                 continue;
440             builder.append(", \"");
441             builder.append(static_cast<CFStringRef>(fontCascadeName.get()));
442             builder.append('"');
443         }
444     }
445     
446     if (behavior == kMACaptionAppearanceBehaviorUseValue)
447         builder.appendLiteral(" !important");
448     builder.append(';');
449     
450     return builder.toString();
451 }
452
453 float CaptionUserPreferencesMediaAF::captionFontSizeScaleAndImportance(bool& important) const
454 {
455     if (testingMode() || !MediaAccessibilityLibrary())
456         return CaptionUserPreferences::captionFontSizeScaleAndImportance(important);
457
458     MACaptionAppearanceBehavior behavior;
459     CGFloat characterScale = CaptionUserPreferences::captionFontSizeScaleAndImportance(important);
460     CGFloat scaleAdjustment = MACaptionAppearanceGetRelativeCharacterSize(kMACaptionAppearanceDomainUser, &behavior);
461
462     if (!scaleAdjustment)
463         return characterScale;
464
465     important = behavior == kMACaptionAppearanceBehaviorUseValue;
466 #if defined(__LP64__) && __LP64__
467     return narrowPrecisionToFloat(scaleAdjustment * characterScale);
468 #else
469     return scaleAdjustment * characterScale;
470 #endif
471 }
472
473 void CaptionUserPreferencesMediaAF::setPreferredLanguage(const String& language)
474 {
475     if (CaptionUserPreferences::captionDisplayMode() == Manual)
476         return;
477
478     if (testingMode() || !MediaAccessibilityLibrary()) {
479         CaptionUserPreferences::setPreferredLanguage(language);
480         return;
481     }
482
483     MACaptionAppearanceAddSelectedLanguage(kMACaptionAppearanceDomainUser, language.createCFString().get());
484 }
485
486 Vector<String> CaptionUserPreferencesMediaAF::preferredLanguages() const
487 {
488     if (testingMode() || !MediaAccessibilityLibrary())
489         return CaptionUserPreferences::preferredLanguages();
490
491     Vector<String> platformLanguages = platformUserPreferredLanguages();
492     Vector<String> override = userPreferredLanguagesOverride();
493     if (!override.isEmpty()) {
494         if (platformLanguages.size() != override.size())
495             return override;
496         for (size_t i = 0; i < override.size(); i++) {
497             if (override[i] != platformLanguages[i])
498                 return override;
499         }
500     }
501
502     CFIndex languageCount = 0;
503     RetainPtr<CFArrayRef> languages = adoptCF(MACaptionAppearanceCopySelectedLanguages(kMACaptionAppearanceDomainUser));
504     if (languages)
505         languageCount = CFArrayGetCount(languages.get());
506
507     if (!languageCount)
508         return CaptionUserPreferences::preferredLanguages();
509
510     Vector<String> userPreferredLanguages;
511     userPreferredLanguages.reserveCapacity(languageCount + platformLanguages.size());
512     for (CFIndex i = 0; i < languageCount; i++)
513         userPreferredLanguages.append(static_cast<CFStringRef>(CFArrayGetValueAtIndex(languages.get(), i)));
514
515     userPreferredLanguages.appendVector(platformLanguages);
516
517     return userPreferredLanguages;
518 }
519
520 void CaptionUserPreferencesMediaAF::setPreferredAudioCharacteristic(const String& characteristic)
521 {
522     if (testingMode() || !MediaAccessibilityLibrary())
523         CaptionUserPreferences::setPreferredAudioCharacteristic(characteristic);
524 }
525
526 Vector<String> CaptionUserPreferencesMediaAF::preferredAudioCharacteristics() const
527 {
528     if (testingMode() || !MediaAccessibilityLibrary() || !canLoad_MediaAccessibility_MAAudibleMediaCopyPreferredCharacteristics())
529         return CaptionUserPreferences::preferredAudioCharacteristics();
530
531     CFIndex characteristicCount = 0;
532     RetainPtr<CFArrayRef> characteristics = adoptCF(MAAudibleMediaCopyPreferredCharacteristics());
533     if (characteristics)
534         characteristicCount = CFArrayGetCount(characteristics.get());
535
536     if (!characteristicCount)
537         return CaptionUserPreferences::preferredAudioCharacteristics();
538
539     Vector<String> userPreferredAudioCharacteristics;
540     userPreferredAudioCharacteristics.reserveCapacity(characteristicCount);
541     for (CFIndex i = 0; i < characteristicCount; i++)
542         userPreferredAudioCharacteristics.append(static_cast<CFStringRef>(CFArrayGetValueAtIndex(characteristics.get(), i)));
543
544     return userPreferredAudioCharacteristics;
545 }
546 #endif // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
547
548 String CaptionUserPreferencesMediaAF::captionsStyleSheetOverride() const
549 {
550     if (testingMode())
551         return CaptionUserPreferences::captionsStyleSheetOverride();
552     
553     StringBuilder captionsOverrideStyleSheet;
554
555 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
556     if (!MediaAccessibilityLibrary())
557         return CaptionUserPreferences::captionsStyleSheetOverride();
558     
559     String captionsColor = captionsTextColorCSS();
560     String edgeStyle = captionsTextEdgeCSS();
561     String fontName = captionsDefaultFontCSS();
562     String background = captionsBackgroundCSS();
563     if (!background.isEmpty() || !captionsColor.isEmpty() || !edgeStyle.isEmpty() || !fontName.isEmpty()) {
564         captionsOverrideStyleSheet.appendLiteral(" video::");
565         captionsOverrideStyleSheet.append(TextTrackCue::cueShadowPseudoId());
566         captionsOverrideStyleSheet.append('{');
567         
568         if (!background.isEmpty())
569             captionsOverrideStyleSheet.append(background);
570         if (!captionsColor.isEmpty())
571             captionsOverrideStyleSheet.append(captionsColor);
572         if (!edgeStyle.isEmpty())
573             captionsOverrideStyleSheet.append(edgeStyle);
574         if (!fontName.isEmpty())
575             captionsOverrideStyleSheet.append(fontName);
576         
577         captionsOverrideStyleSheet.append('}');
578     }
579     
580     String windowColor = captionsWindowCSS();
581     String windowCornerRadius = windowRoundedCornerRadiusCSS();
582     if (!windowColor.isEmpty() || !windowCornerRadius.isEmpty()) {
583         captionsOverrideStyleSheet.appendLiteral(" video::");
584         captionsOverrideStyleSheet.append(VTTCue::cueBackdropShadowPseudoId());
585         captionsOverrideStyleSheet.append('{');
586         
587         if (!windowColor.isEmpty())
588             captionsOverrideStyleSheet.append(windowColor);
589         if (!windowCornerRadius.isEmpty()) {
590             captionsOverrideStyleSheet.append(windowCornerRadius);
591         }
592         
593         captionsOverrideStyleSheet.append('}');
594     }
595 #endif // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
596
597     LOG(Media, "CaptionUserPreferencesMediaAF::captionsStyleSheetOverrideSetting style to:\n%s", captionsOverrideStyleSheet.toString().utf8().data());
598
599     return captionsOverrideStyleSheet.toString();
600 }
601
602 static String languageIdentifier(const String& languageCode)
603 {
604     if (languageCode.isEmpty())
605         return languageCode;
606
607     String lowercaseLanguageCode = languageCode.convertToASCIILowercase();
608
609     // Need 2U here to disambiguate String::operator[] from operator(NSString*, int)[] in a production build.
610     if (lowercaseLanguageCode.length() >= 3 && (lowercaseLanguageCode[2U] == '_' || lowercaseLanguageCode[2U] == '-'))
611         lowercaseLanguageCode.truncate(2);
612
613     return lowercaseLanguageCode;
614 }
615
616 static void buildDisplayStringForTrackBase(StringBuilder& displayName, const TrackBase& track)
617 {
618     String label = track.label();
619     String trackLanguageIdentifier = track.validBCP47Language();
620
621     RetainPtr<CFLocaleRef> currentLocale = adoptCF(CFLocaleCreate(kCFAllocatorDefault, defaultLanguage().createCFString().get()));
622     RetainPtr<CFStringRef> localeIdentifier = adoptCF(CFLocaleCreateCanonicalLocaleIdentifierFromString(kCFAllocatorDefault, trackLanguageIdentifier.createCFString().get()));
623     RetainPtr<CFStringRef> languageCF = adoptCF(CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleLanguageCode, localeIdentifier.get()));
624     String language = languageCF.get();
625
626     if (!label.isEmpty()) {
627         if (language.isEmpty() || label.contains(language))
628             displayName.append(label);
629         else {
630             RetainPtr<CFDictionaryRef> localeDict = adoptCF(CFLocaleCreateComponentsFromLocaleIdentifier(kCFAllocatorDefault, localeIdentifier.get()));
631             if (localeDict) {
632                 CFStringRef countryCode = 0;
633                 String countryName;
634                 
635                 CFDictionaryGetValueIfPresent(localeDict.get(), kCFLocaleCountryCode, (const void **)&countryCode);
636                 if (countryCode) {
637                     RetainPtr<CFStringRef> countryNameCF = adoptCF(CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleCountryCode, countryCode));
638                     countryName = countryNameCF.get();
639                 }
640                 
641                 if (!countryName.isEmpty())
642                     displayName.append(textTrackCountryAndLanguageMenuItemText(label, countryName, language));
643                 else
644                     displayName.append(textTrackLanguageMenuItemText(label, language));
645             }
646         }
647     } else {
648         String languageAndLocale = adoptCF(CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleIdentifier, trackLanguageIdentifier.createCFString().get())).get();
649         if (!languageAndLocale.isEmpty())
650             displayName.append(languageAndLocale);
651         else if (!language.isEmpty())
652             displayName.append(language);
653         else
654             displayName.append(localeIdentifier.get());
655     }
656 }
657
658 static String trackDisplayName(AudioTrack* track)
659 {
660     StringBuilder displayName;
661     buildDisplayStringForTrackBase(displayName, *track);
662     
663     if (displayName.isEmpty())
664         displayName.append(audioTrackNoLabelText());
665
666     if (track->kind() != AudioTrack::descriptionKeyword())
667         return displayName.toString();
668
669     return audioDescriptionTrackSuffixText(displayName.toString());
670 }
671
672 String CaptionUserPreferencesMediaAF::displayNameForTrack(AudioTrack* track) const
673 {
674     return trackDisplayName(track);
675 }
676
677 static String trackDisplayName(TextTrack* track)
678 {
679     if (track == TextTrack::captionMenuOffItem())
680         return textTrackOffMenuItemText();
681     if (track == TextTrack::captionMenuAutomaticItem())
682         return textTrackAutomaticMenuItemText();
683
684     StringBuilder displayNameBuilder;
685     buildDisplayStringForTrackBase(displayNameBuilder, *track);
686
687     if (displayNameBuilder.isEmpty())
688         displayNameBuilder.append(textTrackNoLabelText());
689
690     String displayName = displayNameBuilder.toString();
691
692     if (track->isClosedCaptions()) {
693         displayName = closedCaptionTrackMenuItemText(displayName);
694         if (track->isEasyToRead())
695             displayName = easyReaderTrackMenuItemText(displayName);
696
697         return displayName;
698     }
699
700     if (track->isSDH())
701         displayName = sdhTrackMenuItemText(displayName);
702
703     if (track->containsOnlyForcedSubtitles())
704         displayName = forcedTrackMenuItemText(displayName);
705
706     if (track->isEasyToRead())
707         displayName = easyReaderTrackMenuItemText(displayName);
708
709     return displayName;
710 }
711
712 String CaptionUserPreferencesMediaAF::displayNameForTrack(TextTrack* track) const
713 {
714     return trackDisplayName(track);
715 }
716
717 int CaptionUserPreferencesMediaAF::textTrackSelectionScore(TextTrack* track, HTMLMediaElement* mediaElement) const
718 {
719     CaptionDisplayMode displayMode = captionDisplayMode();
720     if (displayMode == Manual)
721         return 0;
722
723     bool legacyOverride = mediaElement->webkitClosedCaptionsVisible();
724     if (displayMode == AlwaysOn && (!userPrefersSubtitles() && !userPrefersCaptions() && !legacyOverride))
725         return 0;
726     if (track->kind() != TextTrack::Kind::Captions && track->kind() != TextTrack::Kind::Subtitles && track->kind() != TextTrack::Kind::Forced)
727         return 0;
728     if (!track->isMainProgramContent())
729         return 0;
730
731     bool trackHasOnlyForcedSubtitles = track->containsOnlyForcedSubtitles();
732     if (!legacyOverride && ((trackHasOnlyForcedSubtitles && displayMode != ForcedOnly) || (!trackHasOnlyForcedSubtitles && displayMode == ForcedOnly)))
733         return 0;
734
735     Vector<String> userPreferredCaptionLanguages = preferredLanguages();
736
737     if ((displayMode == Automatic && !legacyOverride) || trackHasOnlyForcedSubtitles) {
738
739         if (!mediaElement || !mediaElement->player())
740             return 0;
741
742         String textTrackLanguage = track->validBCP47Language();
743         if (textTrackLanguage.isEmpty())
744             return 0;
745
746         Vector<String> languageList;
747         languageList.reserveCapacity(1);
748
749         String audioTrackLanguage;
750         if (testingMode())
751             audioTrackLanguage = primaryAudioTrackLanguageOverride();
752         else
753             audioTrackLanguage = mediaElement->player()->languageOfPrimaryAudioTrack();
754
755         if (audioTrackLanguage.isEmpty())
756             return 0;
757
758         bool exactMatch;
759         if (trackHasOnlyForcedSubtitles) {
760             languageList.append(audioTrackLanguage);
761             size_t offset = indexOfBestMatchingLanguageInList(textTrackLanguage, languageList, exactMatch);
762
763             // Only consider a forced-only track if it IS in the same language as the primary audio track.
764             if (offset)
765                 return 0;
766         } else {
767             languageList.append(defaultLanguage());
768
769             // Only enable a text track if the current audio track is NOT in the user's preferred language ...
770             size_t offset = indexOfBestMatchingLanguageInList(audioTrackLanguage, languageList, exactMatch);
771             if (!offset)
772                 return 0;
773
774             // and the text track matches the user's preferred language.
775             offset = indexOfBestMatchingLanguageInList(textTrackLanguage, languageList, exactMatch);
776             if (offset)
777                 return 0;
778         }
779
780         userPreferredCaptionLanguages = languageList;
781     }
782
783     int trackScore = 0;
784
785     if (userPrefersCaptions()) {
786         // When the user prefers accessibility tracks, rank is SDH, then CC, then subtitles.
787         if (track->kind() == TextTrack::Kind::Subtitles)
788             trackScore = 1;
789         else if (track->isClosedCaptions())
790             trackScore = 2;
791         else
792             trackScore = 3;
793     } else {
794         // When the user prefers translation tracks, rank is subtitles, then SDH, then CC tracks.
795         if (track->kind() == TextTrack::Kind::Subtitles)
796             trackScore = 3;
797         else if (!track->isClosedCaptions())
798             trackScore = 2;
799         else
800             trackScore = 1;
801     }
802
803     return trackScore + textTrackLanguageSelectionScore(track, userPreferredCaptionLanguages);
804 }
805
806 static bool textTrackCompare(const RefPtr<TextTrack>& a, const RefPtr<TextTrack>& b)
807 {
808     String preferredLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(defaultLanguage()));
809     String aLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(a->validBCP47Language()));
810     String bLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(b->language()));
811
812     // Tracks in the user's preferred language are always at the top of the menu.
813     bool aIsPreferredLanguage = !codePointCompare(aLanguageDisplayName, preferredLanguageDisplayName);
814     bool bIsPreferredLanguage = !codePointCompare(bLanguageDisplayName, preferredLanguageDisplayName);
815     if ((aIsPreferredLanguage || bIsPreferredLanguage) && (aIsPreferredLanguage != bIsPreferredLanguage))
816         return aIsPreferredLanguage;
817
818     // Tracks not in the user's preferred language sort first by language ...
819     if (codePointCompare(aLanguageDisplayName, bLanguageDisplayName))
820         return codePointCompare(aLanguageDisplayName, bLanguageDisplayName) < 0;
821
822     // ... but when tracks have the same language, main program content sorts next highest ...
823     bool aIsMainContent = a->isMainProgramContent();
824     bool bIsMainContent = b->isMainProgramContent();
825     if ((aIsMainContent || bIsMainContent) && (aIsMainContent != bIsMainContent))
826         return aIsMainContent;
827
828     // ... and main program trakcs sort higher than CC tracks ...
829     bool aIsCC = a->isClosedCaptions();
830     bool bIsCC = b->isClosedCaptions();
831     if ((aIsCC || bIsCC) && (aIsCC != bIsCC)) {
832         if (aIsCC)
833             return aIsMainContent;
834         return bIsMainContent;
835     }
836
837     // ... and tracks of the same type and language sort by the menu item text.
838     return codePointCompare(trackDisplayName(a.get()), trackDisplayName(b.get())) < 0;
839 }
840
841 Vector<RefPtr<AudioTrack>> CaptionUserPreferencesMediaAF::sortedTrackListForMenu(AudioTrackList* trackList)
842 {
843     ASSERT(trackList);
844     
845     Vector<RefPtr<AudioTrack>> tracksForMenu;
846     
847     for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
848         AudioTrack* track = trackList->item(i);
849         String language = displayNameForLanguageLocale(track->validBCP47Language());
850         tracksForMenu.append(track);
851     }
852     
853     std::sort(tracksForMenu.begin(), tracksForMenu.end(), [](auto& a, auto& b) {
854         return codePointCompare(trackDisplayName(a.get()), trackDisplayName(b.get())) < 0;
855     });
856     
857     return tracksForMenu;
858 }
859
860 Vector<RefPtr<TextTrack>> CaptionUserPreferencesMediaAF::sortedTrackListForMenu(TextTrackList* trackList)
861 {
862     ASSERT(trackList);
863
864     Vector<RefPtr<TextTrack>> tracksForMenu;
865     HashSet<String> languagesIncluded;
866     CaptionDisplayMode displayMode = captionDisplayMode();
867     bool prefersAccessibilityTracks = userPrefersCaptions();
868     bool filterTrackList = shouldFilterTrackMenu();
869
870     for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
871         TextTrack* track = trackList->item(i);
872         String language = displayNameForLanguageLocale(track->validBCP47Language());
873
874         if (displayMode == Manual) {
875             LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - adding '%s' track with language '%s' because selection mode is 'manual'", track->kindKeyword().string().utf8().data(), language.utf8().data());
876             tracksForMenu.append(track);
877             continue;
878         }
879
880         auto kind = track->kind();
881         if (kind != TextTrack::Kind::Captions && kind != TextTrack::Kind::Descriptions && kind != TextTrack::Kind::Subtitles)
882             continue;
883
884         if (track->containsOnlyForcedSubtitles()) {
885             LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - skipping '%s' track with language '%s' because it contains only forced subtitles", track->kindKeyword().string().utf8().data(), language.utf8().data());
886             continue;
887         }
888         
889         if (track->isEasyToRead()) {
890             LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - adding '%s' track with language '%s' because it is 'easy to read'", track->kindKeyword().string().utf8().data(), language.utf8().data());
891             if (!language.isEmpty())
892                 languagesIncluded.add(language);
893             tracksForMenu.append(track);
894             continue;
895         }
896
897         if (track->mode() == TextTrack::Mode::Showing) {
898             LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - adding '%s' track with language '%s' because it is already visible", track->kindKeyword().string().utf8().data(), language.utf8().data());
899             if (!language.isEmpty())
900                 languagesIncluded.add(language);
901             tracksForMenu.append(track);
902             continue;
903         }
904
905         if (!language.isEmpty() && track->isMainProgramContent()) {
906             bool isAccessibilityTrack = track->kind() == TextTrack::Kind::Captions;
907             if (prefersAccessibilityTracks) {
908                 // In the first pass, include only caption tracks if the user prefers accessibility tracks.
909                 if (!isAccessibilityTrack && filterTrackList) {
910                     LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - skipping '%s' track with language '%s' because it is NOT an accessibility track", track->kindKeyword().string().utf8().data(), language.utf8().data());
911                     continue;
912                 }
913             } else {
914                 // In the first pass, only include the first non-CC or SDH track with each language if the user prefers translation tracks.
915                 if (isAccessibilityTrack && filterTrackList) {
916                     LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - skipping '%s' track with language '%s' because it is an accessibility track", track->kindKeyword().string().utf8().data(), language.utf8().data());
917                     continue;
918                 }
919                 if (languagesIncluded.contains(language) && filterTrackList) {
920                     LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - skipping '%s' track with language '%s' because it is not the first with this language", track->kindKeyword().string().utf8().data(), language.utf8().data());
921                     continue;
922                 }
923             }
924         }
925
926         if (!language.isEmpty())
927             languagesIncluded.add(language);
928         tracksForMenu.append(track);
929
930         LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - adding '%s' track with language '%s', is%s main program content", track->kindKeyword().string().utf8().data(), language.utf8().data(), track->isMainProgramContent() ? "" : " NOT");
931     }
932
933     // Now that we have filtered for the user's accessibility/translation preference, add  all tracks with a unique language without regard to track type.
934     for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
935         TextTrack* track = trackList->item(i);
936         String language = displayNameForLanguageLocale(track->language());
937
938         if (tracksForMenu.contains(track))
939             continue;
940
941         auto kind = track->kind();
942         if (kind != TextTrack::Kind::Captions && kind != TextTrack::Kind::Descriptions && kind != TextTrack::Kind::Subtitles)
943             continue;
944
945         // All candidates with no languge were added the first time through.
946         if (language.isEmpty())
947             continue;
948
949         if (track->containsOnlyForcedSubtitles())
950             continue;
951
952         if (!languagesIncluded.contains(language) && track->isMainProgramContent()) {
953             languagesIncluded.add(language);
954             tracksForMenu.append(track);
955             LOG(Media, "CaptionUserPreferencesMediaAF::sortedTrackListForMenu - adding '%s' track with language '%s' because it is the only track with this language", track->kindKeyword().string().utf8().data(), language.utf8().data());
956         }
957     }
958
959     if (tracksForMenu.isEmpty())
960         return tracksForMenu;
961
962     std::sort(tracksForMenu.begin(), tracksForMenu.end(), textTrackCompare);
963
964     tracksForMenu.insert(0, TextTrack::captionMenuOffItem());
965     tracksForMenu.insert(1, TextTrack::captionMenuAutomaticItem());
966
967     return tracksForMenu;
968 }
969     
970 }
971
972 #endif
973
974 #endif // ENABLE(VIDEO_TRACK)