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