2 * Copyright (C) 2012-2015 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
28 #if ENABLE(VIDEO_TRACK)
30 #include "CaptionUserPreferencesMediaAF.h"
32 #include "AudioTrackList.h"
33 #include "FloatConversion.h"
34 #include "HTMLMediaElement.h"
37 #include "LocalizedStrings.h"
39 #include "MediaControlElements.h"
40 #include "SoftLinking.h"
41 #include "TextTrackList.h"
42 #include "UserStyleSheetTypes.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>
51 #import "WebCoreThreadRun.h"
54 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
55 #include <CoreText/CoreText.h>
56 #include <MediaAccessibility/MediaAccessibility.h>
58 #include "MediaAccessibilitySoftLink.h"
63 #define SOFT_LINK_AVF_FRAMEWORK(Lib) SOFT_LINK_DEBUG_LIBRARY(Lib)
65 #define SOFT_LINK_AVF_FRAMEWORK(Lib) SOFT_LINK_LIBRARY(Lib)
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)
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()
79 #define CTFontDescriptorCopyAttribute softLink_CTFontDescriptorCopyAttribute
81 SOFT_LINK_AVF_FRAMEWORK(CoreMedia)
82 SOFT_LINK_AVF_FRAMEWORK_IMPORT_OPTIONAL(CoreMedia, MTEnableCaption2015Behavior, Boolean, ())
86 SOFT_LINK_FRAMEWORK(MediaToolbox)
87 SOFT_LINK_OPTIONAL(MediaToolbox, MTEnableCaption2015Behavior, Boolean, (), ())
89 #endif // PLATFORM(WIN)
91 #endif // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
95 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
96 static void userCaptionPreferencesChangedNotificationCallback(CFNotificationCenterRef, void* observer, CFStringRef, const void *, CFDictionaryRef)
99 static_cast<CaptionUserPreferencesMediaAF*>(observer)->captionPreferencesChanged();
102 static_cast<CaptionUserPreferencesMediaAF*>(observer)->captionPreferencesChanged();
108 CaptionUserPreferencesMediaAF::CaptionUserPreferencesMediaAF(PageGroup& group)
109 : CaptionUserPreferences(group)
110 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
111 , m_updateStyleSheetTimer(*this, &CaptionUserPreferencesMediaAF::updateTimerFired)
112 , m_listeningForPreferenceChanges(false)
115 static bool initialized;
119 MTEnableCaption2015BehaviorPtrType function = MTEnableCaption2015BehaviorPtr();
120 if (!function || !function())
123 beginBlockingNotifications();
124 CaptionUserPreferences::setCaptionDisplayMode(Manual);
125 setUserPrefersCaptions(false);
126 setUserPrefersSubtitles(false);
127 setUserPrefersTextDescriptions(false);
128 endBlockingNotifications();
132 CaptionUserPreferencesMediaAF::~CaptionUserPreferencesMediaAF()
134 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
135 if (kMAXCaptionAppearanceSettingsChangedNotification)
136 CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), this, kMAXCaptionAppearanceSettingsChangedNotification, 0);
137 if (kMAAudibleMediaSettingsChangedNotification)
138 CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), this, kMAAudibleMediaSettingsChangedNotification, 0);
142 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
144 CaptionUserPreferences::CaptionDisplayMode CaptionUserPreferencesMediaAF::captionDisplayMode() const
146 CaptionDisplayMode internalMode = CaptionUserPreferences::captionDisplayMode();
147 if (internalMode == Manual || testingMode() || !MediaAccessibilityLibrary())
150 MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser);
151 switch (displayType) {
152 case kMACaptionAppearanceDisplayTypeForcedOnly:
155 case kMACaptionAppearanceDisplayTypeAutomatic:
158 case kMACaptionAppearanceDisplayTypeAlwaysOn:
162 ASSERT_NOT_REACHED();
166 void CaptionUserPreferencesMediaAF::setCaptionDisplayMode(CaptionUserPreferences::CaptionDisplayMode mode)
168 if (testingMode() || !MediaAccessibilityLibrary()) {
169 CaptionUserPreferences::setCaptionDisplayMode(mode);
173 if (captionDisplayMode() == Manual)
176 MACaptionAppearanceDisplayType displayType = kMACaptionAppearanceDisplayTypeForcedOnly;
179 displayType = kMACaptionAppearanceDisplayTypeAutomatic;
182 displayType = kMACaptionAppearanceDisplayTypeForcedOnly;
185 displayType = kMACaptionAppearanceDisplayTypeAlwaysOn;
188 ASSERT_NOT_REACHED();
192 MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, displayType);
195 bool CaptionUserPreferencesMediaAF::userPrefersCaptions() const
197 bool captionSetting = CaptionUserPreferences::userPrefersCaptions();
198 if (captionSetting || testingMode() || !MediaAccessibilityLibrary())
199 return captionSetting;
201 RetainPtr<CFArrayRef> captioningMediaCharacteristics = adoptCF(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser));
202 return captioningMediaCharacteristics && CFArrayGetCount(captioningMediaCharacteristics.get());
205 bool CaptionUserPreferencesMediaAF::userPrefersSubtitles() const
207 bool subtitlesSetting = CaptionUserPreferences::userPrefersSubtitles();
208 if (subtitlesSetting || testingMode() || !MediaAccessibilityLibrary())
209 return subtitlesSetting;
211 RetainPtr<CFArrayRef> captioningMediaCharacteristics = adoptCF(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser));
212 return !(captioningMediaCharacteristics && CFArrayGetCount(captioningMediaCharacteristics.get()));
215 void CaptionUserPreferencesMediaAF::updateTimerFired()
217 updateCaptionStyleSheetOverride();
220 void CaptionUserPreferencesMediaAF::setInterestedInCaptionPreferenceChanges()
222 if (m_listeningForPreferenceChanges)
225 if (!MediaAccessibilityLibrary())
228 if (!kMAXCaptionAppearanceSettingsChangedNotification && !canLoad_MediaAccessibility_kMAAudibleMediaSettingsChangedNotification())
231 m_listeningForPreferenceChanges = true;
232 m_registeringForNotification = true;
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;
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);
245 void CaptionUserPreferencesMediaAF::captionPreferencesChanged()
247 if (m_registeringForNotification)
250 if (m_listeningForPreferenceChanges)
251 updateCaptionStyleSheetOverride();
253 CaptionUserPreferences::captionPreferencesChanged();
256 String CaptionUserPreferencesMediaAF::captionsWindowCSS() const
258 MACaptionAppearanceBehavior behavior;
259 RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyWindowColor(kMACaptionAppearanceDomainUser, &behavior));
261 Color windowColor(color.get());
262 if (!windowColor.isValid())
263 windowColor = Color::transparent;
265 bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
266 CGFloat opacity = MACaptionAppearanceGetWindowOpacity(kMACaptionAppearanceDomainUser, &behavior);
268 important = behavior == kMACaptionAppearanceBehaviorUseValue;
269 String windowStyle = colorPropertyCSS(CSSPropertyBackgroundColor, Color(windowColor.red(), windowColor.green(), windowColor.blue(), static_cast<int>(opacity * 255)), important);
274 return makeString(windowStyle, getPropertyNameString(CSSPropertyPadding), ": .4em !important;");
277 String CaptionUserPreferencesMediaAF::captionsBackgroundCSS() const
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);
283 MACaptionAppearanceBehavior behavior;
285 RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyBackgroundColor(kMACaptionAppearanceDomainUser, &behavior));
286 Color backgroundColor(color.get());
287 if (!backgroundColor.isValid())
288 backgroundColor = defaultBackgroundColor;
290 bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
291 CGFloat opacity = MACaptionAppearanceGetBackgroundOpacity(kMACaptionAppearanceDomainUser, &behavior);
293 important = behavior == kMACaptionAppearanceBehaviorUseValue;
294 return colorPropertyCSS(CSSPropertyBackgroundColor, Color(backgroundColor.red(), backgroundColor.green(), backgroundColor.blue(), static_cast<int>(opacity * 255)), important);
297 Color CaptionUserPreferencesMediaAF::captionsTextColor(bool& important) const
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;
306 important = behavior == kMACaptionAppearanceBehaviorUseValue;
307 CGFloat opacity = MACaptionAppearanceGetForegroundOpacity(kMACaptionAppearanceDomainUser, &behavior);
309 important = behavior == kMACaptionAppearanceBehaviorUseValue;
310 return Color(textColor.red(), textColor.green(), textColor.blue(), static_cast<int>(opacity * 255));
313 String CaptionUserPreferencesMediaAF::captionsTextColorCSS() const
316 Color textColor = captionsTextColor(important);
318 if (!textColor.isValid())
319 return emptyString();
321 return colorPropertyCSS(CSSPropertyColor, textColor, important);
324 String CaptionUserPreferencesMediaAF::windowRoundedCornerRadiusCSS() const
326 MACaptionAppearanceBehavior behavior;
327 CGFloat radius = MACaptionAppearanceGetWindowRoundedCornerRadius(kMACaptionAppearanceDomainUser, &behavior);
329 return emptyString();
331 StringBuilder builder;
332 builder.append(getPropertyNameString(CSSPropertyBorderRadius));
333 builder.append(String::format(":%.02fpx", radius));
334 if (behavior == kMACaptionAppearanceBehaviorUseValue)
335 builder.appendLiteral(" !important");
338 return builder.toString();
341 Color CaptionUserPreferencesMediaAF::captionsEdgeColorForTextColor(const Color& textColor) const
343 int distanceFromWhite = differenceSquared(textColor, Color::white);
344 int distanceFromBlack = differenceSquared(textColor, Color::black);
346 if (distanceFromWhite < distanceFromBlack)
347 return textColor.dark();
349 return textColor.light();
352 String CaptionUserPreferencesMediaAF::cssPropertyWithTextEdgeColor(CSSPropertyID id, const String& value, const Color& textColor, bool important) const
354 StringBuilder builder;
356 builder.append(getPropertyNameString(id));
358 builder.append(value);
360 builder.append(captionsEdgeColorForTextColor(textColor).serialized());
362 builder.appendLiteral(" !important");
365 return builder.toString();
368 String CaptionUserPreferencesMediaAF::colorPropertyCSS(CSSPropertyID id, const Color& color, bool important) const
370 StringBuilder builder;
372 builder.append(getPropertyNameString(id));
374 builder.append(color.serialized());
376 builder.appendLiteral(" !important");
379 return builder.toString();
382 String CaptionUserPreferencesMediaAF::captionsTextEdgeCSS() const
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 "));
390 Color color = captionsTextColor(unused);
391 if (!color.isValid())
392 color.setNamedColor("black");
393 color = captionsEdgeColorForTextColor(color);
395 MACaptionAppearanceBehavior behavior;
396 MACaptionAppearanceTextEdgeStyle textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(kMACaptionAppearanceDomainUser, &behavior);
397 switch (textEdgeStyle) {
398 case kMACaptionAppearanceTextEdgeStyleUndefined:
399 case kMACaptionAppearanceTextEdgeStyleNone:
400 return emptyString();
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);
412 ASSERT_NOT_REACHED();
416 return emptyString();
419 String CaptionUserPreferencesMediaAF::captionsDefaultFontCSS() const
421 MACaptionAppearanceBehavior behavior;
423 RetainPtr<CTFontDescriptorRef> font = adoptCF(MACaptionAppearanceCopyFontDescriptorForStyle(kMACaptionAppearanceDomainUser, &behavior, kMACaptionAppearanceFontStyleDefault));
425 return emptyString();
427 RetainPtr<CFTypeRef> name = adoptCF(CTFontDescriptorCopyAttribute(font.get(), kCTFontNameAttribute));
429 return emptyString();
431 StringBuilder builder;
433 builder.append(getPropertyNameString(CSSPropertyFontFamily));
434 builder.appendLiteral(": \"");
435 builder.append(static_cast<CFStringRef>(name.get()));
437 if (behavior == kMACaptionAppearanceBehaviorUseValue)
438 builder.appendLiteral(" !important");
441 return builder.toString();
444 float CaptionUserPreferencesMediaAF::captionFontSizeScaleAndImportance(bool& important) const
446 if (testingMode() || !MediaAccessibilityLibrary())
447 return CaptionUserPreferences::captionFontSizeScaleAndImportance(important);
449 MACaptionAppearanceBehavior behavior;
450 CGFloat characterScale = CaptionUserPreferences::captionFontSizeScaleAndImportance(important);
451 CGFloat scaleAdjustment = MACaptionAppearanceGetRelativeCharacterSize(kMACaptionAppearanceDomainUser, &behavior);
453 if (!scaleAdjustment)
454 return characterScale;
456 important = behavior == kMACaptionAppearanceBehaviorUseValue;
457 #if defined(__LP64__) && __LP64__
458 return narrowPrecisionToFloat(scaleAdjustment * characterScale);
460 return scaleAdjustment * characterScale;
464 void CaptionUserPreferencesMediaAF::setPreferredLanguage(const String& language)
466 if (CaptionUserPreferences::captionDisplayMode() == Manual)
469 if (testingMode() || !MediaAccessibilityLibrary()) {
470 CaptionUserPreferences::setPreferredLanguage(language);
474 MACaptionAppearanceAddSelectedLanguage(kMACaptionAppearanceDomainUser, language.createCFString().get());
477 Vector<String> CaptionUserPreferencesMediaAF::preferredLanguages() const
479 if (testingMode() || !MediaAccessibilityLibrary())
480 return CaptionUserPreferences::preferredLanguages();
482 Vector<String> platformLanguages = platformUserPreferredLanguages();
483 Vector<String> override = userPreferredLanguagesOverride();
484 if (!override.isEmpty()) {
485 if (platformLanguages.size() != override.size())
487 for (size_t i = 0; i < override.size(); i++) {
488 if (override[i] != platformLanguages[i])
493 CFIndex languageCount = 0;
494 RetainPtr<CFArrayRef> languages = adoptCF(MACaptionAppearanceCopySelectedLanguages(kMACaptionAppearanceDomainUser));
496 languageCount = CFArrayGetCount(languages.get());
499 return CaptionUserPreferences::preferredLanguages();
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)));
506 userPreferredLanguages.appendVector(platformLanguages);
508 return userPreferredLanguages;
511 void CaptionUserPreferencesMediaAF::setPreferredAudioCharacteristic(const String& characteristic)
513 if (testingMode() || !MediaAccessibilityLibrary())
514 CaptionUserPreferences::setPreferredAudioCharacteristic(characteristic);
517 Vector<String> CaptionUserPreferencesMediaAF::preferredAudioCharacteristics() const
519 if (testingMode() || !MediaAccessibilityLibrary() || !canLoad_MediaAccessibility_MAAudibleMediaCopyPreferredCharacteristics())
520 return CaptionUserPreferences::preferredAudioCharacteristics();
522 CFIndex characteristicCount = 0;
523 RetainPtr<CFArrayRef> characteristics = adoptCF(MAAudibleMediaCopyPreferredCharacteristics());
525 characteristicCount = CFArrayGetCount(characteristics.get());
527 if (!characteristicCount)
528 return CaptionUserPreferences::preferredAudioCharacteristics();
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)));
535 return userPreferredAudioCharacteristics;
537 #endif // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
539 String CaptionUserPreferencesMediaAF::captionsStyleSheetOverride() const
542 return CaptionUserPreferences::captionsStyleSheetOverride();
544 StringBuilder captionsOverrideStyleSheet;
546 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
547 if (!MediaAccessibilityLibrary())
548 return CaptionUserPreferences::captionsStyleSheetOverride();
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('{');
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);
568 captionsOverrideStyleSheet.append('}');
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('{');
578 if (!windowColor.isEmpty())
579 captionsOverrideStyleSheet.append(windowColor);
580 if (!windowCornerRadius.isEmpty()) {
581 captionsOverrideStyleSheet.append(windowCornerRadius);
584 captionsOverrideStyleSheet.append('}');
586 #endif // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
588 LOG(Media, "CaptionUserPreferencesMediaAF::captionsStyleSheetOverrideSetting sytle to:\n%s", captionsOverrideStyleSheet.toString().utf8().data());
590 return captionsOverrideStyleSheet.toString();
593 static String languageIdentifier(const String& languageCode)
595 if (languageCode.isEmpty())
598 String lowercaseLanguageCode = languageCode.convertToASCIILowercase();
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);
604 return lowercaseLanguageCode;
607 static void buildDisplayStringForTrackBase(StringBuilder& displayName, const TrackBase& track)
609 String label = track.label();
610 String trackLanguageIdentifier = track.language();
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();
617 if (!label.isEmpty()) {
618 if (language.isEmpty() || label.contains(language))
619 displayName.append(label);
621 RetainPtr<CFDictionaryRef> localeDict = adoptCF(CFLocaleCreateComponentsFromLocaleIdentifier(kCFAllocatorDefault, localeIdentifier.get()));
623 CFStringRef countryCode = 0;
626 CFDictionaryGetValueIfPresent(localeDict.get(), kCFLocaleCountryCode, (const void **)&countryCode);
628 RetainPtr<CFStringRef> countryNameCF = adoptCF(CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleCountryCode, countryCode));
629 countryName = countryNameCF.get();
632 if (!countryName.isEmpty())
633 displayName.append(textTrackCountryAndLanguageMenuItemText(label, countryName, language));
635 displayName.append(textTrackLanguageMenuItemText(label, language));
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);
645 displayName.append(localeIdentifier.get());
649 static String trackDisplayName(AudioTrack* track)
651 StringBuilder displayName;
652 buildDisplayStringForTrackBase(displayName, *track);
654 if (displayName.isEmpty())
655 displayName.append(audioTrackNoLabelText());
657 if (track->kind() != AudioTrack::descriptionKeyword())
658 return displayName.toString();
660 return audioDescriptionTrackSuffixText(displayName.toString());
663 String CaptionUserPreferencesMediaAF::displayNameForTrack(AudioTrack* track) const
665 return trackDisplayName(track);
668 static String trackDisplayName(TextTrack* track)
670 if (track == TextTrack::captionMenuOffItem())
671 return textTrackOffMenuItemText();
672 if (track == TextTrack::captionMenuAutomaticItem())
673 return textTrackAutomaticMenuItemText();
675 StringBuilder displayNameBuilder;
676 buildDisplayStringForTrackBase(displayNameBuilder, *track);
678 if (displayNameBuilder.isEmpty())
679 displayNameBuilder.append(textTrackNoLabelText());
681 String displayName = displayNameBuilder.toString();
683 if (track->isClosedCaptions()) {
684 displayName = closedCaptionTrackMenuItemText(displayName);
685 if (track->isEasyToRead())
686 displayName = easyReaderTrackMenuItemText(displayName);
692 displayName = sdhTrackMenuItemText(displayName);
694 if (track->containsOnlyForcedSubtitles())
695 displayName = forcedTrackMenuItemText(displayName);
697 if (track->isEasyToRead())
698 displayName = easyReaderTrackMenuItemText(displayName);
703 String CaptionUserPreferencesMediaAF::displayNameForTrack(TextTrack* track) const
705 return trackDisplayName(track);
708 int CaptionUserPreferencesMediaAF::textTrackSelectionScore(TextTrack* track, HTMLMediaElement* mediaElement) const
710 CaptionDisplayMode displayMode = captionDisplayMode();
711 if (displayMode == Manual)
714 bool legacyOverride = mediaElement->webkitClosedCaptionsVisible();
715 if (displayMode == AlwaysOn && (!userPrefersSubtitles() && !userPrefersCaptions() && !legacyOverride))
717 if (track->kind() != TextTrack::Kind::Captions && track->kind() != TextTrack::Kind::Subtitles && track->kind() != TextTrack::Kind::Forced)
719 if (!track->isMainProgramContent())
722 bool trackHasOnlyForcedSubtitles = track->containsOnlyForcedSubtitles();
723 if (!legacyOverride && ((trackHasOnlyForcedSubtitles && displayMode != ForcedOnly) || (!trackHasOnlyForcedSubtitles && displayMode == ForcedOnly)))
726 Vector<String> userPreferredCaptionLanguages = preferredLanguages();
728 if ((displayMode == Automatic && !legacyOverride) || trackHasOnlyForcedSubtitles) {
730 if (!mediaElement || !mediaElement->player())
733 String textTrackLanguage = track->language();
734 if (textTrackLanguage.isEmpty())
737 Vector<String> languageList;
738 languageList.reserveCapacity(1);
740 String audioTrackLanguage;
742 audioTrackLanguage = primaryAudioTrackLanguageOverride();
744 audioTrackLanguage = mediaElement->player()->languageOfPrimaryAudioTrack();
746 if (audioTrackLanguage.isEmpty())
750 if (trackHasOnlyForcedSubtitles) {
751 languageList.append(audioTrackLanguage);
752 size_t offset = indexOfBestMatchingLanguageInList(textTrackLanguage, languageList, exactMatch);
754 // Only consider a forced-only track if it IS in the same language as the primary audio track.
758 languageList.append(defaultLanguage());
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);
765 // and the text track matches the user's preferred language.
766 offset = indexOfBestMatchingLanguageInList(textTrackLanguage, languageList, exactMatch);
771 userPreferredCaptionLanguages = languageList;
776 if (userPrefersCaptions()) {
777 // When the user prefers accessibility tracks, rank is SDH, then CC, then subtitles.
778 if (track->kind() == TextTrack::Kind::Subtitles)
780 else if (track->isClosedCaptions())
785 // When the user prefers translation tracks, rank is subtitles, then SDH, then CC tracks.
786 if (track->kind() == TextTrack::Kind::Subtitles)
788 else if (!track->isClosedCaptions())
794 return trackScore + textTrackLanguageSelectionScore(track, userPreferredCaptionLanguages);
797 static bool textTrackCompare(const RefPtr<TextTrack>& a, const RefPtr<TextTrack>& b)
799 String preferredLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(defaultLanguage()));
800 String aLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(a->language()));
801 String bLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(b->language()));
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;
809 // Tracks not in the user's preferred language sort first by language ...
810 if (codePointCompare(aLanguageDisplayName, bLanguageDisplayName))
811 return codePointCompare(aLanguageDisplayName, bLanguageDisplayName) < 0;
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;
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)) {
824 return aIsMainContent;
825 return bIsMainContent;
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;
832 Vector<RefPtr<AudioTrack>> CaptionUserPreferencesMediaAF::sortedTrackListForMenu(AudioTrackList* trackList)
836 Vector<RefPtr<AudioTrack>> tracksForMenu;
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);
844 std::sort(tracksForMenu.begin(), tracksForMenu.end(), [](auto& a, auto& b) {
845 return codePointCompare(trackDisplayName(a.get()), trackDisplayName(b.get())) < 0;
848 return tracksForMenu;
851 Vector<RefPtr<TextTrack>> CaptionUserPreferencesMediaAF::sortedTrackListForMenu(TextTrackList* trackList)
855 Vector<RefPtr<TextTrack>> tracksForMenu;
856 HashSet<String> languagesIncluded;
857 CaptionDisplayMode displayMode = captionDisplayMode();
858 bool prefersAccessibilityTracks = userPrefersCaptions();
859 bool filterTrackList = shouldFilterTrackMenu();
861 for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
862 TextTrack* track = trackList->item(i);
863 String language = displayNameForLanguageLocale(track->language());
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);
871 auto kind = track->kind();
872 if (kind != TextTrack::Kind::Captions && kind != TextTrack::Kind::Descriptions && kind != TextTrack::Kind::Subtitles)
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());
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);
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);
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());
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());
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());
917 if (!language.isEmpty())
918 languagesIncluded.add(language);
919 tracksForMenu.append(track);
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");
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());
929 if (tracksForMenu.contains(track))
932 auto kind = track->kind();
933 if (kind != TextTrack::Kind::Captions && kind != TextTrack::Kind::Descriptions && kind != TextTrack::Kind::Subtitles)
936 // All candidates with no languge were added the first time through.
937 if (language.isEmpty())
940 if (track->containsOnlyForcedSubtitles())
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());
950 if (tracksForMenu.isEmpty())
951 return tracksForMenu;
953 std::sort(tracksForMenu.begin(), tracksForMenu.end(), textTrackCompare);
955 tracksForMenu.insert(0, TextTrack::captionMenuOffItem());
956 tracksForMenu.insert(1, TextTrack::captionMenuAutomaticItem());
958 return tracksForMenu;
963 #endif // ENABLE(VIDEO_TRACK)