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