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