From 53ac10fb7430a689168e25b963ae3f75d38b6eb2 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Fri, 20 May 2022 11:52:22 +0200 Subject: [PATCH 01/17] Update version number --- Demo/Demo.xcconfig | 2 +- Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Demo/Demo.xcconfig b/Demo/Demo.xcconfig index 2ab36254..0a07d918 100644 --- a/Demo/Demo.xcconfig +++ b/Demo/Demo.xcconfig @@ -1,5 +1,5 @@ // Version information -MARKETING_VERSION = 7.0.1 +MARKETING_VERSION = 7.0.2 // Deployment targets IPHONEOS_DEPLOYMENT_TARGET = 9.0 diff --git a/Package.swift b/Package.swift index 00f034b8..6862278c 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription struct ProjectSettings { - static let marketingVersion: String = "7.0.1" + static let marketingVersion: String = "7.0.2" } let package = Package( From a592130ab23444206453c27229d61849d2285b11 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Wed, 8 Jun 2022 10:26:41 +0200 Subject: [PATCH 02/17] Update fastlane --- Gemfile.lock | 60 ++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 80005d6c..ba734c7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,20 +8,20 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.554.0) - aws-sdk-core (3.126.0) + aws-partitions (1.597.0) + aws-sdk-core (3.131.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.54.0) - aws-sdk-core (~> 3, >= 3.126.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.57.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.112.0) - aws-sdk-core (~> 3, >= 3.126.0) + aws-sdk-s3 (1.114.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.0.3) @@ -36,8 +36,8 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.3) - excon (0.91.0) - faraday (1.9.3) + excon (0.92.3) + faraday (1.10.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -56,8 +56,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -66,7 +66,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.204.3) + fastlane (2.206.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -106,9 +106,9 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.16.0) + google-apis-androidpublisher_v3 (0.21.0) google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.2) + google-apis-core (0.5.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -121,15 +121,15 @@ GEM google-apis-core (>= 0.4, < 2.a) google-apis-playcustomapp_v1 (0.7.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.11.0) + google-apis-storage_v1 (0.14.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.1) + google-cloud-storage (1.36.2) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -137,20 +137,20 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.1) - faraday (>= 0.17.3, < 2.0) + googleauth (1.1.3) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.6.0) - json (2.6.1) - jwt (2.3.0) + jmespath (1.6.1) + json (2.6.2) + jwt (2.4.1) memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) @@ -161,9 +161,9 @@ GEM optparse (0.1.1) os (1.1.4) plist (3.6.0) - public_suffix (4.0.6) + public_suffix (4.0.7) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -173,9 +173,9 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.0) + signet (0.16.1) addressable (~> 2.8) - faraday (>= 0.17.3, < 2.0) + faraday (>= 0.17.5, < 3.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -192,7 +192,7 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) From ece95bf297064abe5c71109a889ac2284db07163 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 30 May 2022 10:06:37 +0200 Subject: [PATCH 03/17] Update srgssr stream urls --- .../Data/MultiPlayerDemoConfiguration.plist | 2 +- Demo/Resources/Data/SegmentDemoConfiguration.plist | 2 +- Demo/Resources/Data/VideoDemoConfiguration.plist | 8 ++++---- Tests/SRGMediaPlayerTests/PlaybackTestCase.m | 14 +++++++++----- Tests/SRGMediaPlayerTests/RateTestCase.m | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Demo/Resources/Data/MultiPlayerDemoConfiguration.plist b/Demo/Resources/Data/MultiPlayerDemoConfiguration.plist index f1fb71f2..9da8b380 100644 --- a/Demo/Resources/Data/MultiPlayerDemoConfiguration.plist +++ b/Demo/Resources/Data/MultiPlayerDemoConfiguration.plist @@ -14,7 +14,7 @@ name Livestream url - https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8?dw=0 + https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0 name diff --git a/Demo/Resources/Data/SegmentDemoConfiguration.plist b/Demo/Resources/Data/SegmentDemoConfiguration.plist index 035b7713..27c18359 100644 --- a/Demo/Resources/Data/SegmentDemoConfiguration.plist +++ b/Demo/Resources/Data/SegmentDemoConfiguration.plist @@ -363,7 +363,7 @@ name Segments in DVR stream url - https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8 + https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8 segments diff --git a/Demo/Resources/Data/VideoDemoConfiguration.plist b/Demo/Resources/Data/VideoDemoConfiguration.plist index 0abb681a..ef0d8d15 100644 --- a/Demo/Resources/Data/VideoDemoConfiguration.plist +++ b/Demo/Resources/Data/VideoDemoConfiguration.plist @@ -8,13 +8,13 @@ name Livestream url - https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8?dw=0 + https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0 name Livestream with DVR url - https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8 + https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8 name @@ -62,7 +62,7 @@ name How women are taking on the Cresta Run url - https://srgplayerswivod-vh.akamaihd.net/i/44785866/,video,.mp4.csmil/master.m3u8 + https://swi-vod.akamaized.net/videoJson/44785866/master.m3u8 name @@ -82,7 +82,7 @@ name - 10vor10 subject with buggy subtitles (Akamai issue) + 10vor10 subject with buggy subtitles (Akamai MLS 3 issue) url https://hdvodsrforigins3-vh.akamaihd.net/i/assets/video/10vor10/2020/06/10vor10_20200610_215034_21058070_v_webcast_h264_,q40,q10,q20,q30,q50,.mp4.csmil/master.m3u8?start=66.4&end=554.52&caption=srf%2F26a1de9a-7b3e-460f-a3c2-24421b00217a%2Fepisode%2Fde%2Fvod%2Fvod.m3u8%3Ade%3ADeutsch%3Asdh&webvttbaseurl=www.srf.ch%2Fsubtitles diff --git a/Tests/SRGMediaPlayerTests/PlaybackTestCase.m b/Tests/SRGMediaPlayerTests/PlaybackTestCase.m index 1d0a6d41..704d1e38 100644 --- a/Tests/SRGMediaPlayerTests/PlaybackTestCase.m +++ b/Tests/SRGMediaPlayerTests/PlaybackTestCase.m @@ -23,12 +23,12 @@ static NSURL *LiveTestURL(void) { - return [NSURL URLWithString:@"https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8?dw=0"]; + return [NSURL URLWithString:@"https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0"]; } static NSURL *DVRNoTimestampTestURL(void) { - return [NSURL URLWithString:@"https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8"]; + return [NSURL URLWithString:@"https://lsaplus.swisstxt.ch/audio/la-1ere_96.stream/playlist.m3u8"]; } static NSURL *DVRTimestampTestURL(void) @@ -944,7 +944,8 @@ - (void)testPauseAtStreamEnd }]; } -- (void)testFixedStreamEndWithBuggyAkamaiStreamWithSubtitles +// TODO: Remove when Akamai MSL 3 at SRF is fully removed and try with new stream packaging solution (clipped videos) +- (void)testFixedStreamEndWithBuggyAkamaiMLS3StreamWithSubtitles { [self expectationForSingleNotification:SRGMediaPlayerPlaybackStateDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { return [notification.userInfo[SRGMediaPlayerPlaybackStateKey] integerValue] == SRGMediaPlayerPlaybackStatePlaying; @@ -1069,6 +1070,8 @@ - (void)testLiveProperties self.mediaPlayerController.minimumDVRWindowLength = 40.; } + self.mediaPlayerController.minimumDVRWindowLength = 40.; + [self expectationForPredicate:[NSPredicate predicateWithBlock:^BOOL(SRGMediaPlayerController * _Nullable mediaPlayerController, NSDictionary * _Nullable bindings) { return mediaPlayerController.streamType != SRGMediaPlayerStreamTypeUnknown; }] evaluatedWithObject:self.mediaPlayerController handler:nil]; @@ -1079,7 +1082,8 @@ - (void)testLiveProperties XCTAssertEqual(self.mediaPlayerController.streamType, SRGMediaPlayerStreamTypeLive); XCTAssertTrue(self.mediaPlayerController.live); - XCTAssertTrue([NSDate.date timeIntervalSinceDate:self.mediaPlayerController.currentDate] < 1.); + // FIXME: New RTS Couleur 3 stream has #EXT-X-PROGRAM-DATE-TIME and the diff is more than 1 second + XCTAssertTrue([NSDate.date timeIntervalSinceDate:self.mediaPlayerController.currentDate] < 40.); [self expectationForSingleNotification:SRGMediaPlayerPlaybackStateDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { return [notification.userInfo[SRGMediaPlayerPlaybackStateKey] integerValue] == SRGMediaPlayerPlaybackStateIdle; @@ -3082,7 +3086,7 @@ - (void)testDVRStreamEndAbsoluteTolerance return [notification.userInfo[SRGMediaPlayerPlaybackStateKey] integerValue] == SRGMediaPlayerPlaybackStatePlaying; }]; - [self.mediaPlayerController playURL:DVRNoTimestampTestURL() atPosition:nil withSegments:nil userInfo:nil]; + [self.mediaPlayerController playURL:DVRTimestampTestURL() atPosition:nil withSegments:nil userInfo:nil]; [self waitForExpectationsWithTimeout:30. handler:nil]; diff --git a/Tests/SRGMediaPlayerTests/RateTestCase.m b/Tests/SRGMediaPlayerTests/RateTestCase.m index 99f20373..d12ac022 100644 --- a/Tests/SRGMediaPlayerTests/RateTestCase.m +++ b/Tests/SRGMediaPlayerTests/RateTestCase.m @@ -16,12 +16,12 @@ static NSURL *LiveTestURL(void) { - return [NSURL URLWithString:@"https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8?dw=0"]; + return [NSURL URLWithString:@"https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0"]; } static NSURL *DVRTestURL(void) { - return [NSURL URLWithString:@"https://rtsc3video-lh.akamaihd.net/i/rtsc3video_ww@513975/master.m3u8"]; + return [NSURL URLWithString:@"https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8"]; } @import SRGMediaPlayer; From 13747ddda63220e8ca198e8b1598b2c69ff7211f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 13 Jun 2022 18:17:41 +0200 Subject: [PATCH 04/17] Fastlane: set test language and test region --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d828f1b3..9ac69b4d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -91,7 +91,7 @@ def srg_xcodebuild(device, test_build) end def srg_test_xcargs - '-retry-tests-on-failure' + '-retry-tests-on-failure -testLanguage en -testRegion en-US' end def srg_xcodebuild_workspace(test_build) From 8fdd1ea290ec72d4da5331347ad48fc5bcfa8ad4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Tue, 14 Jun 2022 09:50:21 +0200 Subject: [PATCH 05/17] Remove not relevant test for live with timestamp --- Tests/SRGMediaPlayerTests/PlaybackTestCase.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/SRGMediaPlayerTests/PlaybackTestCase.m b/Tests/SRGMediaPlayerTests/PlaybackTestCase.m index 704d1e38..75b96141 100644 --- a/Tests/SRGMediaPlayerTests/PlaybackTestCase.m +++ b/Tests/SRGMediaPlayerTests/PlaybackTestCase.m @@ -1082,8 +1082,6 @@ - (void)testLiveProperties XCTAssertEqual(self.mediaPlayerController.streamType, SRGMediaPlayerStreamTypeLive); XCTAssertTrue(self.mediaPlayerController.live); - // FIXME: New RTS Couleur 3 stream has #EXT-X-PROGRAM-DATE-TIME and the diff is more than 1 second - XCTAssertTrue([NSDate.date timeIntervalSinceDate:self.mediaPlayerController.currentDate] < 40.); [self expectationForSingleNotification:SRGMediaPlayerPlaybackStateDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { return [notification.userInfo[SRGMediaPlayerPlaybackStateKey] integerValue] == SRGMediaPlayerPlaybackStateIdle; From 18d697527a1ab7e46e5168f3a15fad229c3dd901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Tue, 5 Jul 2022 07:03:14 +0200 Subject: [PATCH 06/17] Update gitignore with VS Code files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3882e93a..2b1d1f9e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ xcuserdata /fastlane/*.xml /vendor -/.bundle \ No newline at end of file +/.bundle + +.vscode From bd1e65d4f0a3581067492269efac6d6bba4924e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Fri, 10 Jun 2022 14:21:55 +0200 Subject: [PATCH 07/17] Fix support for forced subtitles --- .../SRGMediaPlayer/SRGMediaPlayerController.m | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 2a3fdc28..cb8ce74b 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -53,6 +53,7 @@ static CMTime SRGSafeStartSeekOffset(void) static AVMediaSelectionOption *SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics); +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, NSString *language); @interface SRGMediaPlayerController () { @private @@ -1539,7 +1540,7 @@ - (void)reloadMediaConfiguration // Setup subtitles. The value `nil` is allowed to disable subtitles entirely. AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; if (subtitleGroup) { - NSArray *subtitleOptions = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleGroup.srgmediaplayer_languageOptions withoutMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]]; + NSArray *subtitleOptions = subtitleGroup.srgmediaplayer_languageOptions; AVMediaSelectionOption *defaultSubtitleOption = SRGMediaPlayerControllerSubtitleDefaultOption(subtitleOptions, audioOption); AVMediaSelectionOption *subtitleOption = self.subtitleConfigurationBlock ? self.subtitleConfigurationBlock(subtitleOptions, audioOption, defaultSubtitleOption) : defaultSubtitleOption; [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; @@ -1865,20 +1866,27 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup return; } - [playerItem selectMediaOption:option inMediaSelectionGroup:group]; - - // If Automatic has been set for subtitles, changing the audio must update the subtitles accordingly - MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser); - if ([characteristic isEqualToString:AVMediaCharacteristicAudible] && displayType == kMACaptionAppearanceDisplayTypeAutomatic) { - // Provide the selected audio option as context information, so that update is consistent when using AirPlay as well - // (we cannot use `-selectMediaOptionAutomaticallyInMediaSelectionGroupWithCharacteristic:`) as the audio selection - // takes more time over AirPlay, yielding the old value for a short while. - AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - if (subtitleGroup) { - AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, option); - [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + if ([characteristic isEqualToString:AVMediaCharacteristicAudible]) { + [playerItem selectMediaOption:option ?: SRGMediaPlayerControllerAutomaticAudioDefaultOption(group.options) inMediaSelectionGroup:group]; + + // If Automatic has been set for subtitles, changing the audio must update the subtitles accordingly + MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser); + if (displayType == kMACaptionAppearanceDisplayTypeAutomatic) { + // Provide the selected audio option as context information, so that update is consistent when using AirPlay as well + // (we cannot use `-selectMediaOptionAutomaticallyInMediaSelectionGroupWithCharacteristic:`) as the audio selection + // takes more time over AirPlay, yielding the old value for a short while. + AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; + if (subtitleGroup) { + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, option); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + } } } + else if ([characteristic isEqualToString:AVMediaCharacteristicLegible]) { + AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; + NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; + [playerItem selectMediaOption:option ?: SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioLanguage) inMediaSelectionGroup:group]; + } [self updateTracksForPlayer:self.player]; } @@ -1941,13 +1949,16 @@ - (BOOL)matchesAutomaticSubtitleSelection return NO; } + NSArray *subtitleOptions = subtitleGroup.srgmediaplayer_languageOptions; AVMediaSelectionOption *subtitleOption = [playerItem srgmediaplayer_selectedMediaOptionInMediaSelectionGroup:subtitleGroup]; - AVMediaSelectionOption *defaultSubtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, audioOption); + AVMediaSelectionOption *defaultSubtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleOptions, audioOption); if (defaultSubtitleOption) { return [defaultSubtitleOption isEqual:subtitleOption]; } else { - return ! subtitleOption || [subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]; + NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; + AVMediaSelectionOption *forcedSubtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); + return ! subtitleOption || [subtitleOption isEqual:forcedSubtitleOption]; } } @@ -2340,14 +2351,13 @@ - (NSString *)description NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; NSString *applicationLanguage = SRGMediaPlayerApplicationLocalization(); - NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - if (characteristics.count != 0 - || (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage])) { + if (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage]) { + NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); return SRGMediaPlayerControllerSubtitleDefaultLanguageOption(subtitleOptions, applicationLanguage, characteristics); } else { - return nil; + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); } } @@ -2371,8 +2381,9 @@ - (NSString *)description break; } - default: { - return nil; + case kMACaptionAppearanceDisplayTypeForcedOnly: { + NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); break; } } @@ -2384,14 +2395,23 @@ - (NSString *)description NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(AVMediaSelectionOption * _Nullable option, NSDictionary * _Nullable bindings) { return [[option.locale objectForKey:NSLocaleLanguageCode] isEqualToString:language]; }]; - NSArray *options = [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withoutMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]] filteredArrayUsingPredicate:predicate]; - // Attempt to find a better match depending on the provided characteristics - if (characteristics.count != 0) { - return [AVMediaSelectionGroup mediaSelectionOptionsFromArray:options withMediaCharacteristics:characteristics].firstObject ?: options.firstObject; + // Attempt to find a match for the provided characteristics + NSArray *options = [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:options withMediaCharacteristics:characteristics] filteredArrayUsingPredicate:predicate]; + if (options.count != 0) { + return options.firstObject; } - // No characteristics provided. At least attempt to avoid closed captions + // Consider forced subtitles for the provided language else { - return [AVMediaSelectionGroup mediaSelectionOptionsFromArray:options withoutMediaCharacteristics:@[AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]].firstObject ?: options.firstObject; + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, language); } } + +// Return the forced subtitle option having some language in the provided list, `nil` if none +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, NSString *language) +{ + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(AVMediaSelectionOption * _Nullable option, NSDictionary * _Nullable bindings) { + return [[option.locale objectForKey:NSLocaleLanguageCode] isEqualToString:language]; + }]; + return [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]] filteredArrayUsingPredicate:predicate].firstObject; +} From 4105c8151335fac852509a945722ac5ff1293a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Fri, 10 Jun 2022 15:57:44 +0200 Subject: [PATCH 08/17] Update subtitles when changing the audio track --- .../SRGMediaPlayer/SRGMediaPlayerController.m | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index cb8ce74b..384544f4 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -1867,25 +1867,41 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup } if ([characteristic isEqualToString:AVMediaCharacteristicAudible]) { - [playerItem selectMediaOption:option ?: SRGMediaPlayerControllerAutomaticAudioDefaultOption(group.options) inMediaSelectionGroup:group]; + AVMediaSelectionOption *audioOption = option ?: SRGMediaPlayerControllerAutomaticAudioDefaultOption(group.options); + [playerItem selectMediaOption:audioOption inMediaSelectionGroup:group]; // If Automatic has been set for subtitles, changing the audio must update the subtitles accordingly MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser); - if (displayType == kMACaptionAppearanceDisplayTypeAutomatic) { + AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; + if (subtitleGroup) { // Provide the selected audio option as context information, so that update is consistent when using AirPlay as well // (we cannot use `-selectMediaOptionAutomaticallyInMediaSelectionGroupWithCharacteristic:`) as the audio selection // takes more time over AirPlay, yielding the old value for a short while. - AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - if (subtitleGroup) { - AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, option); - [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + switch (displayType) { + case kMACaptionAppearanceDisplayTypeAutomatic: { + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, audioOption); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + break; + } + + case kMACaptionAppearanceDisplayTypeForcedOnly: { + NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleGroup.srgmediaplayer_languageOptions, audioLanguage); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + break; + } + + case kMACaptionAppearanceDisplayTypeAlwaysOn: { + break; + } } } } else if ([characteristic isEqualToString:AVMediaCharacteristicLegible]) { AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; - [playerItem selectMediaOption:option ?: SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioLanguage) inMediaSelectionGroup:group]; + AVMediaSelectionOption *subtitleOption = option ?: SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioLanguage); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:group]; } [self updateTracksForPlayer:self.player]; From 5687c2fb6efb32db6a3b5f225183d41933d62592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 08:39:06 +0200 Subject: [PATCH 09/17] Improve code locality --- Sources/SRGMediaPlayer/SRGMediaPlayerController.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 384544f4..489bccbd 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -1870,13 +1870,13 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup AVMediaSelectionOption *audioOption = option ?: SRGMediaPlayerControllerAutomaticAudioDefaultOption(group.options); [playerItem selectMediaOption:audioOption inMediaSelectionGroup:group]; - // If Automatic has been set for subtitles, changing the audio must update the subtitles accordingly - MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser); + // Update subtitles to match the audio track if needed. AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; if (subtitleGroup) { // Provide the selected audio option as context information, so that update is consistent when using AirPlay as well // (we cannot use `-selectMediaOptionAutomaticallyInMediaSelectionGroupWithCharacteristic:`) as the audio selection // takes more time over AirPlay, yielding the old value for a short while. + MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser); switch (displayType) { case kMACaptionAppearanceDisplayTypeAutomatic: { AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, audioOption); @@ -1959,15 +1959,15 @@ - (BOOL)matchesAutomaticSubtitleSelection return NO; } - AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; if (! subtitleGroup) { return NO; } NSArray *subtitleOptions = subtitleGroup.srgmediaplayer_languageOptions; - AVMediaSelectionOption *subtitleOption = [playerItem srgmediaplayer_selectedMediaOptionInMediaSelectionGroup:subtitleGroup]; + AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; AVMediaSelectionOption *defaultSubtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleOptions, audioOption); + AVMediaSelectionOption *subtitleOption = [playerItem srgmediaplayer_selectedMediaOptionInMediaSelectionGroup:subtitleGroup]; if (defaultSubtitleOption) { return [defaultSubtitleOption isEqual:subtitleOption]; } @@ -2360,7 +2360,7 @@ - (NSString *)description return [AVMediaSelectionGroup mediaSelectionOptionsFromArray:options withMediaCharacteristics:characteristics].firstObject ?: options.firstObject; } -// For Automatic mode, return the default subtitle option which should be selected in the provided list (an audio option can be provided to help find the best match). +// For Automatic mode return the default subtitle option which should be selected in the provided list (an audio option can be provided to help find the best match). static AVMediaSelectionOption *SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption) { NSCParameterAssert(subtitleOptions); From 3be184b32fc5f10b15e80da6d008ab997b5c4de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 08:41:27 +0200 Subject: [PATCH 10/17] Rewrite for better readability --- Sources/SRGMediaPlayer/SRGMediaPlayerController.m | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 489bccbd..07faebf3 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -1899,9 +1899,14 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup } else if ([characteristic isEqualToString:AVMediaCharacteristicLegible]) { AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; - NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; - AVMediaSelectionOption *subtitleOption = option ?: SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioLanguage); - [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:group]; + if (option) { + [playerItem selectMediaOption:option inMediaSelectionGroup:group]; + } + else { + NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioLanguage); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:group]; + } } [self updateTracksForPlayer:self.player]; From 4dbce0fbb41facbb7ec3ba2b93f313bcb7ea607e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 08:42:30 +0200 Subject: [PATCH 11/17] Fix misleading variable name --- Sources/SRGMediaPlayer/SRGMediaPlayerController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 07faebf3..42bd2db9 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -1810,8 +1810,8 @@ - (AVMediaSelectionOption *)selectedOptionForPlayer:(AVPlayer *)player withMedia return nil; } - AVMediaSelectionGroup *audioGroup = [asset mediaSelectionGroupForMediaCharacteristic:mediaCharacteristic]; - return audioGroup ? [playerItem srgmediaplayer_selectedMediaOptionInMediaSelectionGroup:audioGroup] : nil; + AVMediaSelectionGroup *group = [asset mediaSelectionGroupForMediaCharacteristic:mediaCharacteristic]; + return group ? [playerItem srgmediaplayer_selectedMediaOptionInMediaSelectionGroup:group] : nil; } - (void)updateTracksForPlayer:(AVPlayer *)player From c0497225ac7887c875c3e4e4fcab8270a33e9660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 09:07:21 +0200 Subject: [PATCH 12/17] Fix Always on subtitle lookup --- Sources/SRGMediaPlayer/SRGMediaPlayerController.m | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 42bd2db9..cdab103d 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -52,7 +52,7 @@ static CMTime SRGSafeStartSeekOffset(void) static AVMediaSelectionOption *SRGMediaPlayerControllerAutomaticAudioDefaultOption(NSArray *audioOptions); static AVMediaSelectionOption *SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); -static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics); +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics); static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, NSString *language); @interface SRGMediaPlayerController () { @@ -2375,7 +2375,7 @@ - (NSString *)description if (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage]) { NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - return SRGMediaPlayerControllerSubtitleDefaultLanguageOption(subtitleOptions, applicationLanguage, characteristics); + return SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, applicationLanguage, characteristics); } else { return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); @@ -2398,7 +2398,7 @@ - (NSString *)description case kMACaptionAppearanceDisplayTypeAlwaysOn: { NSString *lastSelectedLanguage = SRGMediaAccessibilityCaptionAppearanceLastSelectedLanguage(kMACaptionAppearanceDomainUser); NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - return SRGMediaPlayerControllerSubtitleDefaultLanguageOption(subtitleOptions, lastSelectedLanguage, characteristics); + return SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, lastSelectedLanguage, characteristics); break; } @@ -2410,15 +2410,16 @@ - (NSString *)description } } -// Return the default subtitle option which should be selected in the provided list, matching a specific language and characteristics. -static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics) +// Return the default "Always on" subtitle option which should be selected in the provided list, matching a specific language and characteristics. +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics) { NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(AVMediaSelectionOption * _Nullable option, NSDictionary * _Nullable bindings) { return [[option.locale objectForKey:NSLocaleLanguageCode] isEqualToString:language]; }]; - // Attempt to find a match for the provided characteristics - NSArray *options = [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:options withMediaCharacteristics:characteristics] filteredArrayUsingPredicate:predicate]; + // Attempt to find a match for the provided characteristics (forced subtitles excepted) + NSArray *alwaysOnOptions = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withoutMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]]; + NSArray *options = [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:alwaysOnOptions withMediaCharacteristics:characteristics] filteredArrayUsingPredicate:predicate]; if (options.count != 0) { return options.firstObject; } From b2d42595bd86b6f2a7787a5f8f0319b46da8ee70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 09:14:20 +0200 Subject: [PATCH 13/17] Only display relevant automatic selection information --- .../SRGMediaPlayer/SRGPlaybackSettingsViewController~ios.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/SRGMediaPlayer/SRGPlaybackSettingsViewController~ios.m b/Sources/SRGMediaPlayer/SRGPlaybackSettingsViewController~ios.m index 30eec294..17fec889 100755 --- a/Sources/SRGMediaPlayer/SRGPlaybackSettingsViewController~ios.m +++ b/Sources/SRGMediaPlayer/SRGPlaybackSettingsViewController~ios.m @@ -587,7 +587,12 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell = [self subtitleCellForTableView:tableView]; AVMediaSelectionOption *selectedOption = [self.mediaPlayerController selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible]; - cell.detailTextLabel.text = SRGHintForMediaSelectionOption(selectedOption) ?: SRGMediaPlayerLocalizedString(@"None", @"Label displayed when no subtitles have been selected in automatic mode"); + if (! [selectedOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]) { + cell.detailTextLabel.text = SRGHintForMediaSelectionOption(selectedOption); + } + else { + cell.detailTextLabel.text = nil; + } } else { cell = [self defaultCellForTableView:tableView]; From 2ad948645ad2d0945c753ce00681011b487f6b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 13:53:46 +0200 Subject: [PATCH 14/17] Add tests for forced subtitles --- Tests/SRGMediaPlayerTests/TracksTestCase.m | 133 ++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/Tests/SRGMediaPlayerTests/TracksTestCase.m b/Tests/SRGMediaPlayerTests/TracksTestCase.m index 8a6be0e1..20f60c5d 100644 --- a/Tests/SRGMediaPlayerTests/TracksTestCase.m +++ b/Tests/SRGMediaPlayerTests/TracksTestCase.m @@ -193,6 +193,130 @@ - (void)testSubtitlesNotificationsWithSubtitleConfiguration [self waitForExpectationsWithTimeout:30. handler:nil]; } +- (void)testForcedOnlyBehaviorWithForcedSubtitles +{ + MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeForcedOnly); + + [self expectationForSingleNotification:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { + XCTAssertNil(notification.userInfo[SRGMediaPlayerPreviousTrackKey]); + + AVMediaSelectionOption *subtitleOption = notification.userInfo[SRGMediaPlayerTrackKey]; + XCTAssertEqualObjects([subtitleOption.locale objectForKey:NSLocaleLanguageCode], @"en"); + XCTAssertTrue([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicLegible]); + XCTAssertTrue([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]); + return YES; + }]; + + [self.mediaPlayerController playURL:InternationalTracksOnDemandTestURL()]; + + [self waitForExpectationsWithTimeout:30. handler:nil]; + + XCTAssertEqualObjects([self selectedLanguageCodeInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible], @"en"); +} + +- (void)testAutomaticBehaviorWithForcedSubtitles +{ + MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeAutomatic); + + [self expectationForSingleNotification:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { + XCTAssertNil(notification.userInfo[SRGMediaPlayerPreviousTrackKey]); + + AVMediaSelectionOption *subtitleOption = notification.userInfo[SRGMediaPlayerTrackKey]; + XCTAssertEqualObjects([subtitleOption.locale objectForKey:NSLocaleLanguageCode], @"en"); + XCTAssertTrue([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicLegible]); + XCTAssertTrue([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]); + return YES; + }]; + + [self.mediaPlayerController playURL:InternationalTracksOnDemandTestURL()]; + + [self waitForExpectationsWithTimeout:30. handler:nil]; + + XCTAssertEqualObjects([self selectedLanguageCodeInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible], @"en"); +} + +- (void)testAlwaysOnBehaviorWithForcedSubtitles +{ + MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeAlwaysOn); + MACaptionAppearanceAddSelectedLanguage(kMACaptionAppearanceDomainUser, (__bridge CFStringRef _Nonnull)@"en"); + + [self expectationForSingleNotification:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { + XCTAssertNil(notification.userInfo[SRGMediaPlayerPreviousTrackKey]); + + AVMediaSelectionOption *subtitleOption = notification.userInfo[SRGMediaPlayerTrackKey]; + XCTAssertEqualObjects([subtitleOption.locale objectForKey:NSLocaleLanguageCode], @"en"); + XCTAssertTrue([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicLegible]); + XCTAssertFalse([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]); + return YES; + }]; + + [self.mediaPlayerController playURL:InternationalTracksOnDemandTestURL()]; + + [self waitForExpectationsWithTimeout:30. handler:nil]; + + XCTAssertEqualObjects([self selectedLanguageCodeInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible], @"en"); +} + +- (void)testForcedOnlyBehaviorWithoutForcedSubtitles +{ + MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeForcedOnly); + + [self expectationForElapsedTimeInterval:2. withHandler:nil]; + + id subtitleTrackObserver = [NSNotificationCenter.defaultCenter addObserverForName:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController queue:nil usingBlock:^(NSNotification * _Nonnull note) { + XCTFail(@"No subtitle track change is expected"); + }]; + + [self.mediaPlayerController playURL:SwissTracksOnDemandTestURL()]; + + [self waitForExpectationsWithTimeout:30. handler:^(NSError * _Nullable error) { + [NSNotificationCenter.defaultCenter removeObserver:subtitleTrackObserver]; + }]; + + XCTAssertNil([self selectedLanguageCodeInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible]); +} + +- (void)testAutomaticBehaviorWithoutForcedSubtitles +{ + MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeAutomatic); + + [self expectationForElapsedTimeInterval:2. withHandler:nil]; + + id subtitleTrackObserver = [NSNotificationCenter.defaultCenter addObserverForName:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController queue:nil usingBlock:^(NSNotification * _Nonnull note) { + XCTFail(@"No subtitle track change is expected"); + }]; + + [self.mediaPlayerController playURL:SwissTracksOnDemandTestURL()]; + + [self waitForExpectationsWithTimeout:30. handler:^(NSError * _Nullable error) { + [NSNotificationCenter.defaultCenter removeObserver:subtitleTrackObserver]; + }]; + + XCTAssertNil([self selectedLanguageCodeInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible]); +} + +- (void)testAlwaysOnBehaviorWithoutForcedSubtitles +{ + MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeAlwaysOn); + MACaptionAppearanceAddSelectedLanguage(kMACaptionAppearanceDomainUser, (__bridge CFStringRef _Nonnull)@"fr"); + + [self expectationForSingleNotification:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { + XCTAssertNil(notification.userInfo[SRGMediaPlayerPreviousTrackKey]); + + AVMediaSelectionOption *subtitleOption = notification.userInfo[SRGMediaPlayerTrackKey]; + XCTAssertEqualObjects([subtitleOption.locale objectForKey:NSLocaleLanguageCode], @"fr"); + XCTAssertTrue([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicLegible]); + XCTAssertFalse([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]); + return YES; + }]; + + [self.mediaPlayerController playURL:SwissTracksOnDemandTestURL()]; + + [self waitForExpectationsWithTimeout:30. handler:nil]; + + XCTAssertEqualObjects([self selectedLanguageCodeInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicLegible], @"fr"); +} + - (void)testSubtitleStyleCustomization { MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, kMACaptionAppearanceDisplayTypeAutomatic); @@ -227,8 +351,13 @@ - (void)testMediaConfigurationReloadDuringPlayback [self waitForExpectationsWithTimeout:30. handler:nil]; [self expectationForSingleNotification:SRGMediaPlayerSubtitleTrackDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { - XCTAssertNil([[notification.userInfo[SRGMediaPlayerPreviousTrackKey] locale] objectForKey:NSLocaleLanguageCode]); - XCTAssertEqualObjects([[notification.userInfo[SRGMediaPlayerTrackKey] locale] objectForKey:NSLocaleLanguageCode], @"ja"); + AVMediaSelectionOption *previousSubtitleOption = notification.userInfo[SRGMediaPlayerPreviousTrackKey]; + XCTAssertEqualObjects([previousSubtitleOption.locale objectForKey:NSLocaleLanguageCode], @"en"); + XCTAssertTrue([previousSubtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]); + + AVMediaSelectionOption *subtitleOption = notification.userInfo[SRGMediaPlayerTrackKey]; + XCTAssertEqualObjects([subtitleOption.locale objectForKey:NSLocaleLanguageCode], @"ja"); + XCTAssertFalse([subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]); return YES; }]; From a1894b03947b2f77ce5ccec43c89764dc25976f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 14:18:48 +0200 Subject: [PATCH 15/17] Use forced subtitles as fallback for Automatic mode as well --- Sources/SRGMediaPlayer/SRGMediaPlayerController.m | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index cdab103d..2c03ea25 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -2375,11 +2375,13 @@ - (NSString *)description if (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage]) { NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - return SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, applicationLanguage, characteristics); - } - else { - return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, applicationLanguage, characteristics); + if (subtitleOption) { + return subtitleOption; + } } + + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); } // Return the default subtitle option which should be selected in the provided list, based on on `MediaAccessibility` settings (an audio option can be provided to help From 060c60cf3b3039f1918dbb935d773dd6addbe4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 14:54:22 +0200 Subject: [PATCH 16/17] Improve selection when characteristics cannot be exactly matched --- .../SRGMediaPlayer/SRGMediaPlayerController.m | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 2c03ea25..6b4e86ac 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -52,8 +52,8 @@ static CMTime SRGSafeStartSeekOffset(void) static AVMediaSelectionOption *SRGMediaPlayerControllerAutomaticAudioDefaultOption(NSArray *audioOptions); static AVMediaSelectionOption *SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); -static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics); -static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, NSString *language); +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics, AVMediaSelectionOption *audioOption); +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); @interface SRGMediaPlayerController () { @private @@ -1885,8 +1885,7 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup } case kMACaptionAppearanceDisplayTypeForcedOnly: { - NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; - AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleGroup.srgmediaplayer_languageOptions, audioLanguage); + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleGroup.srgmediaplayer_languageOptions, audioOption); [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; break; } @@ -1903,8 +1902,7 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup [playerItem selectMediaOption:option inMediaSelectionGroup:group]; } else { - NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; - AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioLanguage); + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioOption); [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:group]; } } @@ -1977,8 +1975,7 @@ - (BOOL)matchesAutomaticSubtitleSelection return [defaultSubtitleOption isEqual:subtitleOption]; } else { - NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; - AVMediaSelectionOption *forcedSubtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); + AVMediaSelectionOption *forcedSubtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioOption); return ! subtitleOption || [subtitleOption isEqual:forcedSubtitleOption]; } } @@ -2375,13 +2372,13 @@ - (NSString *)description if (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage]) { NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, applicationLanguage, characteristics); + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, applicationLanguage, characteristics, audioOption); if (subtitleOption) { return subtitleOption; } } - return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioOption); } // Return the default subtitle option which should be selected in the provided list, based on on `MediaAccessibility` settings (an audio option can be provided to help @@ -2400,42 +2397,50 @@ - (NSString *)description case kMACaptionAppearanceDisplayTypeAlwaysOn: { NSString *lastSelectedLanguage = SRGMediaAccessibilityCaptionAppearanceLastSelectedLanguage(kMACaptionAppearanceDomainUser); NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - return SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, lastSelectedLanguage, characteristics); + return SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, lastSelectedLanguage, characteristics, audioOption); break; } case kMACaptionAppearanceDisplayTypeForcedOnly: { - NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; - return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioLanguage); + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioOption); break; } } } // Return the default "Always on" subtitle option which should be selected in the provided list, matching a specific language and characteristics. -static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics) +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics, AVMediaSelectionOption *audioOption) { NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(AVMediaSelectionOption * _Nullable option, NSDictionary * _Nullable bindings) { return [[option.locale objectForKey:NSLocaleLanguageCode] isEqualToString:language]; }]; - // Attempt to find a match for the provided characteristics (forced subtitles excepted) - NSArray *alwaysOnOptions = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withoutMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]]; - NSArray *options = [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:alwaysOnOptions withMediaCharacteristics:characteristics] filteredArrayUsingPredicate:predicate]; - if (options.count != 0) { - return options.firstObject; + // Only consider unforced subtitles matching the desired language + NSArray *alwaysOnOptions = [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withoutMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]] filteredArrayUsingPredicate:predicate]; + + // Attempt to find an exact match for the provided characteristics + AVMediaSelectionOption *exactOption = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:alwaysOnOptions withMediaCharacteristics:characteristics].firstObject; + if (exactOption) { + return exactOption; } - // Consider forced subtitles for the provided language - else { - return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, language); + + // Ignore characteristics. Accessibility settings like SDH preference will be ignored but having a subtitle track + // as a fallback is still better than having no track at all. + AVMediaSelectionOption *fallbackOption = alwaysOnOptions.firstObject; + if (fallbackOption) { + return fallbackOption; } + + // No match found. Use forced subtitles matching the audio language. + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioOption); } // Return the forced subtitle option having some language in the provided list, `nil` if none -static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, NSString *language) +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption) { + NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(AVMediaSelectionOption * _Nullable option, NSDictionary * _Nullable bindings) { - return [[option.locale objectForKey:NSLocaleLanguageCode] isEqualToString:language]; + return [[option.locale objectForKey:NSLocaleLanguageCode] isEqualToString:audioLanguage]; }]; return [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]] filteredArrayUsingPredicate:predicate].firstObject; } From 8a6efa16e872160d164dc3b3a9c169dc5926882b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Mon, 13 Jun 2022 15:27:51 +0200 Subject: [PATCH 17/17] Improve code locality --- Sources/SRGMediaPlayer/SRGMediaPlayerController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 6b4e86ac..e99b031e 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -1897,11 +1897,11 @@ - (void)selectMediaOption:(AVMediaSelectionOption *)option inMediaSelectionGroup } } else if ([characteristic isEqualToString:AVMediaCharacteristicLegible]) { - AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; if (option) { [playerItem selectMediaOption:option inMediaSelectionGroup:group]; } else { + AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioOption); [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:group]; }