Bug 226038

Summary: [Modern Media Controls] REGRESSION(r254389) media controls needs the full list of language preferences for ordering tracks
Product: WebKit Reporter: Devin Rousso <hi>
Component: MediaAssignee: Devin Rousso <hi>
Status: RESOLVED FIXED    
Severity: Normal CC: benjamin, cdumez, cmarcelo, dino, eric.carlson, ews-watchlist, glenn, hi, jer.noble, keith_miller, mark.lam, mmaxfield, msaboff, peng.liu6, philipj, saam, sergio, tzagallo, webkit-bug-importer
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
See Also: https://bugs.webkit.org/show_bug.cgi?id=225994
https://bugs.webkit.org/show_bug.cgi?id=225900
https://bugs.webkit.org/show_bug.cgi?id=226059
https://bugs.webkit.org/show_bug.cgi?id=226123
https://bugs.webkit.org/show_bug.cgi?id=226252
Bug Depends on: 200043    
Bug Blocks:    
Attachments:
Description Flags
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch
ews-feeder: commit-queue-
Patch
ews-feeder: commit-queue-
Patch
none
Patch
none
Patch
none
Patch
none
Patch none

Description Devin Rousso 2021-05-20 12:46:53 PDT
media controls need access to the full list of language preferences in order to properly sort/order the list of tracks in the controls UI (e.g. if a `<video>` has subtitles for English, Spanish, and French, and the user has English (default) and French (alternate) configured in the Language & Region view of System Preferences, WebKit should order the subtitles list English, French, and then Spanish)
Comment 1 Devin Rousso 2021-05-20 12:58:47 PDT
Created attachment 429210 [details]
Patch
Comment 2 Myles C. Maxfield 2021-05-20 17:12:37 PDT
Comment on attachment 429210 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429210&action=review

> Source/JavaScriptCore/runtime/JSGlobalObject.h:253
> +    typedef String (*DefaultLanguageFunctionPtr)(ShouldMinimizeLanguages);

I think this is philosophically the wrong direction. I can't think of a case where JavaScript would ever even have the possibility of passing No here. Can we consider reverting this typedef to what it was originally, and using a wrapper function instead?

> Source/WTF/wtf/Language.cpp:197
> +static Vector<String>& platformPreferredLanguagesOverride()
> +{
> +    static LazyNeverDestroyed<Vector<String>> override;
> +    static std::once_flag onceKey;
> +    std::call_once(onceKey, [&] {
> +        override.construct();
> +    });
> +    return override;
> +}
> +
> +Vector<String> platformUserPreferredLanguagesOverride()
> +{
> +    return platformPreferredLanguagesOverride();
> +}
> +
> +void overridePlatformUserPreferredLanguages(const Vector<String>& override)
> +{
> +    platformPreferredLanguagesOverride() = override;
> +    languageDidChange();
> +}

The "platform" prefix seems wrong here, since these functions are not platform-specific. (platformUserPreferredLanguages() is in LanguageCF.cpp and is thus platform-specific.)

It seems unfortunate to have two levels of overriding for what is (conceptually) a single feature. Can we do this instead by using Obj-C to swizzle out the runtime of +[NSLocale minimizedLanguagesFromLanguages:] in the web process (using injected bundle)?

> Source/WTF/wtf/cf/LanguageCF.cpp:66
> +    static LazyNeverDestroyed<Vector<String>> languages;
> +    static std::once_flag onceKey;
> +    std::call_once(onceKey, [&] {
> +        languages.construct();
> +    });

What's the point of using "Lazy"NeverDestroyed if you always immediately construct it? Why not just use regular NeverDestroyed? I see that preferredLanguages() does this too.... maybe this pattern is more thread safe or something?

> Source/WTF/wtf/cf/LanguageCF.cpp:147
> +                    auto baseLanguage = baseLanguageFromLanguage(language);

Why are we doing this? I don't see any explanation at all in the changeLog. What's the relation between where or not we should be minimizing languages and whether or not base languages appear as duplicates in the list? I don't think we should be doing this.

> Source/WTF/wtf/cf/LanguageCF.cpp:153
>      }

Can we delete the double return at the end of this function while we're here?
Comment 3 Myles C. Maxfield 2021-05-20 17:15:33 PDT
Comment on attachment 429210 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429210&action=review

> Source/WTF/wtf/cf/LanguageCF.cpp:125
>      if (userPreferredLanguages.isEmpty()) {

I wonder, since we're making changes in this area, if it makes sense to move this cache up to the non-platform-specific layer. All platforms will want to cache results.
Comment 4 Chris Dumez 2021-05-20 17:19:39 PDT
Comment on attachment 429210 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429210&action=review

>> Source/WTF/wtf/cf/LanguageCF.cpp:66
>> +    });
> 
> What's the point of using "Lazy"NeverDestroyed if you always immediately construct it? Why not just use regular NeverDestroyed? I see that preferredLanguages() does this too.... maybe this pattern is more thread safe or something?

We want to be using LazyNeverDestroyed<> with a call_once for global statics that can be accessed from multiple threads. This is because static variable initialization is not thread-safe in WebKit (because we disable that at compiler level for perf reasons).

That said, it looks to me here that ALL call sites grab the preferredLanguagesMutex lock before calling this. As a result, it does not seem we need any additional synchronization and we should be able to use NeverDestroyed and get rid of the call_once().
Comment 5 Devin Rousso 2021-05-21 12:58:10 PDT
Comment on attachment 429210 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429210&action=review

>> Source/JavaScriptCore/runtime/JSGlobalObject.h:253
>> +    typedef String (*DefaultLanguageFunctionPtr)(ShouldMinimizeLanguages);
> 
> I think this is philosophically the wrong direction. I can't think of a case where JavaScript would ever even have the possibility of passing No here. Can we consider reverting this typedef to what it was originally, and using a wrapper function instead?

Sure.  I'll try using a `[] () { return defaultLanguage(); }` inline lambda.

>> Source/WTF/wtf/Language.cpp:197
>> +}
> 
> The "platform" prefix seems wrong here, since these functions are not platform-specific. (platformUserPreferredLanguages() is in LanguageCF.cpp and is thus platform-specific.)
> 
> It seems unfortunate to have two levels of overriding for what is (conceptually) a single feature. Can we do this instead by using Obj-C to swizzle out the runtime of +[NSLocale minimizedLanguagesFromLanguages:] in the web process (using injected bundle)?

The real thing I'm after here is replacing `CFLocaleCopyPreferredLanguages` as we don't want to have to modify the bots to have more than one user configured language.  I agree that the naming is odd.  My thinking was that we're overriding how the platform gets the list of user languages.  I'll see about renaming.

>>> Source/WTF/wtf/cf/LanguageCF.cpp:66
>>> +    });
>> 
>> What's the point of using "Lazy"NeverDestroyed if you always immediately construct it? Why not just use regular NeverDestroyed? I see that preferredLanguages() does this too.... maybe this pattern is more thread safe or something?
> 
> We want to be using LazyNeverDestroyed<> with a call_once for global statics that can be accessed from multiple threads. This is because static variable initialization is not thread-safe in WebKit (because we disable that at compiler level for perf reasons).
> 
> That said, it looks to me here that ALL call sites grab the preferredLanguagesMutex lock before calling this. As a result, it does not seem we need any additional synchronization and we should be able to use NeverDestroyed and get rid of the call_once().

I'll change this to be a `CheckedLock` with `WTF_REQUIRES_LOCK` and `NeverDestroyed`.

>> Source/WTF/wtf/cf/LanguageCF.cpp:125
>>      if (userPreferredLanguages.isEmpty()) {
> 
> I wonder, since we're making changes in this area, if it makes sense to move this cache up to the non-platform-specific layer. All platforms will want to cache results.

While I do agree that caching could be done at a higher level, there appear to be other uses of `platformUserPreferredLanguages`.  I'll see if they still need to be there and if not I'll do this.

>> Source/WTF/wtf/cf/LanguageCF.cpp:147
>> +                    auto baseLanguage = baseLanguageFromLanguage(language);
> 
> Why are we doing this? I don't see any explanation at all in the changeLog. What's the relation between where or not we should be minimizing languages and whether or not base languages appear as duplicates in the list? I don't think we should be doing this.

This matches part of the implementation of `+[NSLocale minimizedLanguagesFromLanguages:]`.  The reason why we want this is that media tracks can specify `lang="en"` (in addition to `lang="en-US"`) and so as a result we want `"en"` to match if the user prefers `"en-US"` (and vice versa).  I'll add a comment in the ChangeLog.
Comment 6 Devin Rousso 2021-05-21 15:22:03 PDT
Created attachment 429350 [details]
Patch
Comment 7 Devin Rousso 2021-05-21 18:41:54 PDT
Created attachment 429383 [details]
Patch
Comment 8 Devin Rousso 2021-05-21 23:59:10 PDT
Created attachment 429405 [details]
Patch
Comment 9 Chris Dumez 2021-05-22 00:18:20 PDT
Comment on attachment 429405 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429405&action=review

> Source/WTF/wtf/Language.h:38
> +enum class ShouldMinimizeLanguages { Yes, No };

We prefer to put the No first in the enum, so that its value is 0, and Yes is 1.

> Source/WTF/wtf/cf/LanguageCF.cpp:93
> +            CFArrayAppendValue(array.get(), item.isolatedCopy().createCFString().get());

Why isolateCopy just to create a CFString?

> Source/WTF/wtf/cocoa/LanguageCocoa.mm:70
> +#if PLATFORM(IOS)

Could use #elif

Also, are we sure we want iOS only? Not Marzipan, WatchOS, TVOS?

> Source/WebCore/page/CaptionUserPreferencesMediaAF.cpp:480
> +            temp.reserveCapacity(captionLanguagesCount + preferredLanguages.size());

reserveInitialCapacity() since you've just created the vector.

> Source/WebCore/page/CaptionUserPreferencesMediaAF.cpp:482
> +                temp.append(static_cast<CFStringRef>(CFArrayGetValueAtIndex(captionLanguages.get(), i)));

Can use uncheckedAppend() since you've reserved capacity.
Comment 10 Devin Rousso 2021-05-22 16:48:15 PDT
Created attachment 429441 [details]
Patch
Comment 11 Devin Rousso 2021-05-22 22:34:49 PDT
Created attachment 429458 [details]
Patch
Comment 12 Devin Rousso 2021-05-23 11:43:51 PDT
Created attachment 429469 [details]
Patch
Comment 13 Devin Rousso 2021-05-23 13:05:47 PDT
Created attachment 429472 [details]
Patch
Comment 14 Devin Rousso 2021-05-23 13:10:41 PDT
Created attachment 429473 [details]
Patch
Comment 15 Devin Rousso 2021-05-23 13:19:47 PDT
Created attachment 429476 [details]
Patch
Comment 16 Devin Rousso 2021-05-23 16:14:47 PDT
Created attachment 429483 [details]
Patch
Comment 17 Devin Rousso 2021-05-24 07:57:55 PDT
Created attachment 429530 [details]
Patch
Comment 18 Chris Dumez 2021-05-24 08:36:41 PDT
Comment on attachment 429530 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429530&action=review

> Source/WTF/wtf/Language.cpp:40
> +static CheckedLock cachedPlatformPreferredLanguagesLock;

Note that we should use Lock in new code, not CheckedLock. I am about to remove the CheckedLock alias.

Sorry about all the reshuffling.
Comment 19 Myles C. Maxfield 2021-05-24 15:59:34 PDT
Comment on attachment 429530 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429530&action=review

> Source/WTF/wtf/Language.cpp:219
> +Vector<String> initialPlatformUserPreferredLanguagesOverrideForTesting()
> +{
> +    Locker locker { initialPlatformPreferredLanguagesOverrideForTestingLock };
> +    return initialPlatformPreferredLanguagesOverrideForTesting();
>  }

Devin and I talked about a way to remove this.

> Source/WTF/wtf/cf/LanguageCF.cpp:115
> +            // Convert languages with country codes to equivalent languages without country codes (if possible).
> +            // e.g. "en-US" becomes "en", which is useful when trying to match HTML `lang="en"`.
> +            auto baseLanguage = baseLanguageFromLanguage(platformLanguage);
> +            if (baseLanguage && baseLanguage != platformLanguage && CFStringCompare(baseLanguage.get(), platformLanguage, 0) != kCFCompareEqualTo)
> +                languages.append(httpStyleLanguageCode(baseLanguage.get(), shouldMinimizeLanguages));

Devin and I talked about a way to avoid this.
Comment 20 Devin Rousso 2021-05-24 19:56:42 PDT
Created attachment 429616 [details]
Patch
Comment 21 Myles C. Maxfield 2021-05-25 11:00:28 PDT
Comment on attachment 429616 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429616&action=review

I can't review the media and subtitle specific code, but the language code stuff looks good.

> Source/JavaScriptCore/Configurations/JavaScriptCore.xcconfig:35
> +// Needed by `Source/WTF/wtf/cocoa/LanguageCocoa.mm`.
> +WK_INTERNATIONAL_SUPPORT_LDFLAGS = -framework InternationalSupport;
> +
> +OTHER_LDFLAGS_BASE = $(OTHER_LDFLAGS_HIDE_SYMBOLS) -iframework $(SDK_DIR)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks -force_load "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/libWTF.a" $(WK_INTERNATIONAL_SUPPORT_LDFLAGS);

Why is this necessary? The only change to LanguageCocoa.mm is a comment.

> Source/WebCore/page/CaptionUserPreferencesMediaAF.cpp:486
> +    if (!testingMode() && MediaAccessibilityLibrary()) {
> +        auto captionLanguages = adoptCF(MACaptionAppearanceCopySelectedLanguages(kMACaptionAppearanceDomainUser));
> +        CFIndex captionLanguagesCount = captionLanguages ? CFArrayGetCount(captionLanguages.get()) : 0;
> +        if (captionLanguagesCount) {
> +            Vector<String> temp;
> +            temp.reserveInitialCapacity(captionLanguagesCount + preferredLanguages.size());
> +            for (CFIndex i = 0; i < captionLanguagesCount; i++)
> +                temp.uncheckedAppend(static_cast<CFStringRef>(CFArrayGetValueAtIndex(captionLanguages.get(), i)));
> +            temp.appendVector(WTFMove(preferredLanguages));
> +            preferredLanguages = WTFMove(temp);
>          }
>      }

I think WebKit prefers early return style like the red code below this, rather than the triangle style here.

> LayoutTests/media/modern-media-controls/tracks-support/sorted-by-user-preferred-languages.html:36
> +    shouldBecomeDifferent("shadowRoot.querySelector('button.tracks')", "null", () => {

This is a big scary. Does this only work because we're inside a test?

> WebKitLibraries/WebKitPrivateFrameworkStubs/Mac/101500/InternationalSupport.framework/InternationalSupport.tbd:7
> +--- !tapi-tbd-v3
> +archs:           [ x86_64 ]
> +install-name:    '/System/Library/PrivateFrameworks/InternationalSupport.framework/InternationalSupport'
> +platform: zippered
> +exports:
> +  - archs:           [ x86_64 ]
> +...

Are you sure you need these .tbd files? I don't think we're calling anything additional from InternationalSupport in this patch.
Comment 22 Myles C. Maxfield 2021-05-25 11:00:55 PDT
Comment on attachment 429616 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429616&action=review

>> LayoutTests/media/modern-media-controls/tracks-support/sorted-by-user-preferred-languages.html:36
>> +    shouldBecomeDifferent("shadowRoot.querySelector('button.tracks')", "null", () => {
> 
> This is a big scary. Does this only work because we're inside a test?

*bit scary
Comment 23 Eric Carlson 2021-05-25 11:57:05 PDT
Comment on attachment 429616 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429616&action=review

The media specific parts look fine to me modulo the comment about the test.

> LayoutTests/media/modern-media-controls/tracks-support/sorted-by-user-preferred-languages.html:9
> +    <track src="../../lorem-ipsum.vtt" kind="captions"     srclang="en" label="en">

As we discussed, I think it would be less confusing to not use the same string for `label and `srclang`.
Comment 24 Devin Rousso 2021-05-25 14:14:53 PDT
Comment on attachment 429616 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=429616&action=review

>> Source/JavaScriptCore/Configurations/JavaScriptCore.xcconfig:35
>> +OTHER_LDFLAGS_BASE = $(OTHER_LDFLAGS_HIDE_SYMBOLS) -iframework $(SDK_DIR)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks -force_load "$(BUILT_PRODUCTS_DIR)/DerivedSources/JavaScriptCore/libWTF.a" $(WK_INTERNATIONAL_SUPPORT_LDFLAGS);
> 
> Why is this necessary? The only change to LanguageCocoa.mm is a comment.

I was using this in previous patches to get non-internal EWS working.  It may not be necessary anymore.  Will try without it.

>>> LayoutTests/media/modern-media-controls/tracks-support/sorted-by-user-preferred-languages.html:36
>>> +    shouldBecomeDifferent("shadowRoot.querySelector('button.tracks')", "null", () => {
>> 
>> This is a big scary. Does this only work because we're inside a test?
> 
> *bit scary

Yes this is from `Internals::shadowRoot`.  We use this extensively in media controls tests.  It is not exposed to the web.

>> WebKitLibraries/WebKitPrivateFrameworkStubs/Mac/101500/InternationalSupport.framework/InternationalSupport.tbd:7
>> +...
> 
> Are you sure you need these .tbd files? I don't think we're calling anything additional from InternationalSupport in this patch.

ditto (JavaScriptCore.xcconfig:35)
Comment 25 Devin Rousso 2021-05-25 14:50:15 PDT
Created attachment 429694 [details]
Patch
Comment 26 EWS 2021-05-25 17:15:10 PDT
Committed r278064 (238145@main): <https://commits.webkit.org/238145@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 429694 [details].
Comment 27 Radar WebKit Bug Importer 2021-05-25 17:16:49 PDT
<rdar://problem/78484571>