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 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/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/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) 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( diff --git a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m index 2a3fdc28..e99b031e 100644 --- a/Sources/SRGMediaPlayer/SRGMediaPlayerController.m +++ b/Sources/SRGMediaPlayer/SRGMediaPlayerController.m @@ -52,7 +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 *SRGMediaPlayerControllerSubtitleDefaultLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics); +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(NSArray *subtitleOptions, NSString *language, NSArray *characteristics, AVMediaSelectionOption *audioOption); +static AVMediaSelectionOption *SRGMediaPlayerControllerSubtitleForcedLanguageOption(NSArray *subtitleOptions, AVMediaSelectionOption *audioOption); @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]; @@ -1809,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 @@ -1865,18 +1866,44 @@ - (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. + if ([characteristic isEqualToString:AVMediaCharacteristicAudible]) { + AVMediaSelectionOption *audioOption = option ?: SRGMediaPlayerControllerAutomaticAudioDefaultOption(group.options); + [playerItem selectMediaOption:audioOption inMediaSelectionGroup:group]; + + // Update subtitles to match the audio track if needed. AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; if (subtitleGroup) { - AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, option); - [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup: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); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + break; + } + + case kMACaptionAppearanceDisplayTypeForcedOnly: { + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleGroup.srgmediaplayer_languageOptions, audioOption); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:subtitleGroup]; + break; + } + + case kMACaptionAppearanceDisplayTypeAlwaysOn: { + break; + } + } + } + } + else if ([characteristic isEqualToString:AVMediaCharacteristicLegible]) { + if (option) { + [playerItem selectMediaOption:option inMediaSelectionGroup:group]; + } + else { + AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(group.options, audioOption); + [playerItem selectMediaOption:subtitleOption inMediaSelectionGroup:group]; } } @@ -1935,19 +1962,21 @@ - (BOOL)matchesAutomaticSubtitleSelection return NO; } - AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; AVMediaSelectionGroup *subtitleGroup = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; if (! subtitleGroup) { return NO; } + NSArray *subtitleOptions = subtitleGroup.srgmediaplayer_languageOptions; + AVMediaSelectionOption *audioOption = [self selectedMediaOptionInMediaSelectionGroupWithCharacteristic:AVMediaCharacteristicAudible]; + AVMediaSelectionOption *defaultSubtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleOptions, audioOption); AVMediaSelectionOption *subtitleOption = [playerItem srgmediaplayer_selectedMediaOptionInMediaSelectionGroup:subtitleGroup]; - AVMediaSelectionOption *defaultSubtitleOption = SRGMediaPlayerControllerAutomaticSubtitleDefaultOption(subtitleGroup.srgmediaplayer_languageOptions, audioOption); if (defaultSubtitleOption) { return [defaultSubtitleOption isEqual:subtitleOption]; } else { - return ! subtitleOption || [subtitleOption hasMediaCharacteristic:AVMediaCharacteristicContainsOnlyForcedSubtitles]; + AVMediaSelectionOption *forcedSubtitleOption = SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioOption); + return ! subtitleOption || [subtitleOption isEqual:forcedSubtitleOption]; } } @@ -2333,22 +2362,23 @@ - (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); NSString *audioLanguage = [audioOption.locale objectForKey:NSLocaleLanguageCode]; NSString *applicationLanguage = SRGMediaPlayerApplicationLocalization(); - NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - if (characteristics.count != 0 - || (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage])) { - return SRGMediaPlayerControllerSubtitleDefaultLanguageOption(subtitleOptions, applicationLanguage, characteristics); - } - else { - return nil; + if (audioLanguage && ! [audioLanguage isEqualToString:applicationLanguage]) { + NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); + AVMediaSelectionOption *subtitleOption = SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, applicationLanguage, characteristics, audioOption); + if (subtitleOption) { + return subtitleOption; + } } + + 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 @@ -2367,31 +2397,50 @@ - (NSString *)description case kMACaptionAppearanceDisplayTypeAlwaysOn: { NSString *lastSelectedLanguage = SRGMediaAccessibilityCaptionAppearanceLastSelectedLanguage(kMACaptionAppearanceDomainUser); NSArray *characteristics = CFBridgingRelease(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser)); - return SRGMediaPlayerControllerSubtitleDefaultLanguageOption(subtitleOptions, lastSelectedLanguage, characteristics); + return SRGMediaPlayerControllerSubtitleDefaultAlwaysOnLanguageOption(subtitleOptions, lastSelectedLanguage, characteristics, audioOption); break; } - default: { - return nil; + case kMACaptionAppearanceDisplayTypeForcedOnly: { + return SRGMediaPlayerControllerSubtitleForcedLanguageOption(subtitleOptions, audioOption); break; } } } -// 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, AVMediaSelectionOption *audioOption) { 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; + // 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; } - // No characteristics provided. At least attempt to avoid closed captions - else { - return [AVMediaSelectionGroup mediaSelectionOptionsFromArray:options withoutMediaCharacteristics:@[AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]].firstObject ?: options.firstObject; + + // 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, 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:audioLanguage]; + }]; + return [[AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitleOptions withMediaCharacteristics:@[AVMediaCharacteristicContainsOnlyForcedSubtitles]] filteredArrayUsingPredicate:predicate].firstObject; } 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]; diff --git a/Tests/SRGMediaPlayerTests/PlaybackTestCase.m b/Tests/SRGMediaPlayerTests/PlaybackTestCase.m index 1d0a6d41..75b96141 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,6 @@ - (void)testLiveProperties XCTAssertEqual(self.mediaPlayerController.streamType, SRGMediaPlayerStreamTypeLive); XCTAssertTrue(self.mediaPlayerController.live); - XCTAssertTrue([NSDate.date timeIntervalSinceDate:self.mediaPlayerController.currentDate] < 1.); [self expectationForSingleNotification:SRGMediaPlayerPlaybackStateDidChangeNotification object:self.mediaPlayerController handler:^BOOL(NSNotification * _Nonnull notification) { return [notification.userInfo[SRGMediaPlayerPlaybackStateKey] integerValue] == SRGMediaPlayerPlaybackStateIdle; @@ -3082,7 +3084,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; 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; }]; 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)