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