From 1e472833b7693fb80ae0da32ccee4e871f290282 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Fri, 12 Aug 2022 19:46:20 +0200 Subject: [PATCH 01/35] Bump iOS and tvOS versions --- Xcode/Shared/Targets/iOS/Common.xcconfig | 4 ++-- Xcode/Shared/Targets/tvOS/Common.xcconfig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Xcode/Shared/Targets/iOS/Common.xcconfig b/Xcode/Shared/Targets/iOS/Common.xcconfig index b06411c20..f8086d8c2 100755 --- a/Xcode/Shared/Targets/iOS/Common.xcconfig +++ b/Xcode/Shared/Targets/iOS/Common.xcconfig @@ -1,8 +1,8 @@ #include "Xcode/Shared/Common.xcconfig" // Version information -MARKETING_VERSION = 3.6.4 -CURRENT_PROJECT_VERSION = 393 +MARKETING_VERSION = 3.6.5 +CURRENT_PROJECT_VERSION = 394 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY=1,2 diff --git a/Xcode/Shared/Targets/tvOS/Common.xcconfig b/Xcode/Shared/Targets/tvOS/Common.xcconfig index 84807be73..93be910c1 100755 --- a/Xcode/Shared/Targets/tvOS/Common.xcconfig +++ b/Xcode/Shared/Targets/tvOS/Common.xcconfig @@ -1,8 +1,8 @@ #include "Xcode/Shared/Common.xcconfig" // Version information -MARKETING_VERSION = 1.6.4 -CURRENT_PROJECT_VERSION = 48 +MARKETING_VERSION = 1.6.5 +CURRENT_PROJECT_VERSION = 49 SDKROOT = appletvos TARGETED_DEVICE_FAMILY=3 From b849aae2283f9d62cd16113b8652823ed9a1f49d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 15 Aug 2022 20:28:12 +0200 Subject: [PATCH 02/35] Update read me with Android TV --- docs/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index b257cf29c..8fc5ca4b6 100755 --- a/docs/README.md +++ b/docs/README.md @@ -4,13 +4,13 @@ Play SRG is the [SRG SSR (Swiss Broadcasting Corporation)](https://www.srgssr.ch/en/who-we-are/organisation/) audio and video platform, provided as a distinct service for each of its business units ([RSI](https://www.rsi.ch), [RTR](https://www.rtr.ch), [RTS](https://www.rts.ch), [SRF](https://www.srf.ch) and [SWI](https://www.swissinfo.ch)). This repository contains the source code of the Play SRG applications for iOS and tvOS. -The Play platform is more generally available on the web and on Android phones: +The Play platform is more generally available on the web, on Android phones and devices running Android TV: | Platform | Play RSI | Play RTR | Play RTS | Play SRF | Play SWI | |:-- |:--:|:--:|:--:|:--:|:--:| -| iOS / tvOS | [πŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-rsi/id920753497) | [πŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-rtr/id920754925) | [πŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-rts/id920754415) | [πŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-srf/id638194352) | [πŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-swi/id920785201) | -| Android | [πŸ€–](https://play.google.com/store/apps/details?id=ch.rsi.player) | [πŸ€–](https://play.google.com/store/apps/details?id=ch.rtr.player) | [πŸ€–](https://play.google.com/store/apps/details?id=ch.rts.player) | [πŸ€–](https://play.google.com/store/apps/details?id=ch.srf.mobile.srfplayer) | [πŸ€–](https://play.google.com/store/apps/details?id=ch.swissinfo.player) | -| Web | [πŸ–₯](https://www.rsi.ch/play) | [πŸ–₯](https://www.rtr.ch/play) | [πŸ–₯](https://www.rts.ch/play) | [πŸ–₯](https://www.srf.ch/play) | [πŸ–₯](https://www.swissinfo.ch/play) | +| iOS / tvOS | [πŸŽπŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-rsi/id920753497) | [πŸŽπŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-rtr/id920754925) | [πŸŽπŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-rts/id920754415) | [πŸŽπŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-srf/id638194352) | [πŸŽπŸ“±πŸ“Ί](https://itunes.apple.com/ch/app/play-swi/id920785201) | +| Android / Android TV | [πŸ€–πŸ“±πŸ“Ί](https://play.google.com/store/apps/details?id=ch.rsi.player) | [πŸ€–πŸ“±πŸ“Ί](https://play.google.com/store/apps/details?id=ch.rtr.player) | [πŸ€–πŸ“±πŸ“Ί](https://play.google.com/store/apps/details?id=ch.rts.player) | [πŸ€–πŸ“±πŸ“Ί](https://play.google.com/store/apps/details?id=ch.srf.mobile.srfplayer) | [πŸ€–πŸ“±πŸ“Ί](https://play.google.com/store/apps/details?id=ch.swissinfo.player) | +| Web | [πŸ–₯πŸ“±πŸ’»](https://www.rsi.ch/play) | [πŸ–₯πŸ“±πŸ’»](https://www.rtr.ch/play) | [πŸ–₯πŸ“±πŸ’»](https://www.rts.ch/play) | [πŸ–₯πŸ“±πŸ’»](https://www.srf.ch/play) | [πŸ–₯πŸ“±πŸ’»](https://www.swissinfo.ch/play) | ![Screenshots](README-images/iphone-screenshots.png) @@ -91,7 +91,7 @@ Simply open the project with Xcode and wait until all dependencies have been ret ## Releasing binaries -The proprietary project uses [fastlane](https://fastlane.tools/) for releasing binaries, either for internal purposes or for the AppStore. +The proprietary project uses [fastlane](https://fastlane.tools/) to [release binaries](RELEASE_CHECKLIST.md), either for internal purposes or for the AppStore. ## Specifications From 06430901b8493db03d63d736304dc11ff7b8af6c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Tue, 16 Aug 2022 09:47:49 +0200 Subject: [PATCH 03/35] Add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..127e2494b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ + +### Motivation and Context + +> Why is this change required? What problem does it solve? + +### Description + +> Please describe the changes and how to test. + +### Checklist + +- [ ] The branch has been rebased onto the `develop` branch. +- The code followed the code style: + - [ ] `swiftlint` has run to ensure the *Swift* code style is valid. + - [ ] `rubocop -a` has run to ensure the *Ruby* code style is valid. +- [ ] Remote configuration properties have been properly documented (if relevant). +- [ ] The documentation has been updated (if relevant). +- [ ] Issues are linked to the PR, if any. From 98acfa1ce3ef3e076ad9cfc7ead07c5a543b9d97 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Fri, 12 Aug 2022 19:32:20 +0200 Subject: [PATCH 04/35] Update recommendation playlist if new subdivision is not in the playlist --- Application/Sources/Helpers/Playlist.h | 1 + Application/Sources/Player/MediaPlayerViewController.m | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/Application/Sources/Helpers/Playlist.h b/Application/Sources/Helpers/Playlist.h index bde99f77e..841eb7f27 100755 --- a/Application/Sources/Helpers/Playlist.h +++ b/Application/Sources/Helpers/Playlist.h @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithURN:(NSString *)URN; @property (nonatomic, nullable, readonly) NSString *recommendationUid; +@property (nonatomic, nullable, readonly) NSArray *medias; @end diff --git a/Application/Sources/Player/MediaPlayerViewController.m b/Application/Sources/Player/MediaPlayerViewController.m index 91bc19f70..a1376235b 100755 --- a/Application/Sources/Player/MediaPlayerViewController.m +++ b/Application/Sources/Player/MediaPlayerViewController.m @@ -1655,6 +1655,16 @@ - (void)letterboxView:(SRGLetterboxView *)letterboxView didSelectSubdivision:(SR [letterboxView setUserInterfaceHidden:! UIAccessibilityIsVoiceOverRunning() animated:YES]; }); } + + SRGLetterboxController *letterboxController = letterboxView.controller; + Playlist *playlist = [letterboxController.playlistDataSource isKindOfClass:Playlist.class] ? (Playlist *)letterboxController.playlistDataSource : nil; + + NSString *keyPath = [NSString stringWithFormat:@"@distinctUnionOfObjects.%@", @keypath(SRGMedia.new, URN)]; + if (! [[playlist.medias valueForKeyPath:keyPath] containsObject:subdivision.URN]) { + Playlist *playlist = PlaylistForURN(subdivision.URN); + letterboxController.playlistDataSource = playlist; + letterboxController.playbackTransitionDelegate = playlist; + } } - (void)letterboxView:(SRGLetterboxView *)letterboxView didSelectAudioLanguageCode:(NSString *)languageCode From 8110ea6fd6041ca0dcb8c3c93de126220ac44ca3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Fri, 12 Aug 2022 19:32:59 +0200 Subject: [PATCH 05/35] Use key path instead of string --- Application/Sources/Downloads/DownloadSession.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Application/Sources/Downloads/DownloadSession.m b/Application/Sources/Downloads/DownloadSession.m index 34b99cf4c..53fb499be 100755 --- a/Application/Sources/Downloads/DownloadSession.m +++ b/Application/Sources/Downloads/DownloadSession.m @@ -100,7 +100,8 @@ - (void)setNeedsStateUpdate - (BOOL)addDownload:(Download *)download { - if ([[self.downloads.allValues valueForKeyPath:@"@distinctUnionOfObjects.URN"] containsObject:download.URN]) { + NSString *keyPath = [NSString stringWithFormat:@"@distinctUnionOfObjects.%@", @keypath(Download.new, URN)]; + if ([[self.downloads.allValues valueForKeyPath:keyPath] containsObject:download.URN]) { return NO; } From 63e89c752b10a942900b9ec8a6ddfc554eaef3af Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Fri, 12 Aug 2022 19:37:49 +0200 Subject: [PATCH 06/35] Update playlist only if it's a new chapter and not in the current playlist --- .../Sources/Player/MediaPlayerViewController.m | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Application/Sources/Player/MediaPlayerViewController.m b/Application/Sources/Player/MediaPlayerViewController.m index a1376235b..691e3e57a 100755 --- a/Application/Sources/Player/MediaPlayerViewController.m +++ b/Application/Sources/Player/MediaPlayerViewController.m @@ -1656,14 +1656,16 @@ - (void)letterboxView:(SRGLetterboxView *)letterboxView didSelectSubdivision:(SR }); } - SRGLetterboxController *letterboxController = letterboxView.controller; - Playlist *playlist = [letterboxController.playlistDataSource isKindOfClass:Playlist.class] ? (Playlist *)letterboxController.playlistDataSource : nil; - - NSString *keyPath = [NSString stringWithFormat:@"@distinctUnionOfObjects.%@", @keypath(SRGMedia.new, URN)]; - if (! [[playlist.medias valueForKeyPath:keyPath] containsObject:subdivision.URN]) { - Playlist *playlist = PlaylistForURN(subdivision.URN); - letterboxController.playlistDataSource = playlist; - letterboxController.playbackTransitionDelegate = playlist; + if ([subdivision isKindOfClass:SRGChapter.class]) { + SRGLetterboxController *letterboxController = letterboxView.controller; + Playlist *playlist = [letterboxController.playlistDataSource isKindOfClass:Playlist.class] ? (Playlist *)letterboxController.playlistDataSource : nil; + + NSString *keyPath = [NSString stringWithFormat:@"@distinctUnionOfObjects.%@", @keypath(SRGMedia.new, URN)]; + if (! [[playlist.medias valueForKeyPath:keyPath] containsObject:subdivision.URN]) { + Playlist *playlist = PlaylistForURN(subdivision.URN); + letterboxController.playlistDataSource = playlist; + letterboxController.playbackTransitionDelegate = playlist; + } } } From 2dd95fcec2b98a14108bc727b4dcb062d0c5f12c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Fri, 12 Aug 2022 20:25:50 +0200 Subject: [PATCH 07/35] Unicity is not required Simplify the key path --- Application/Sources/Player/MediaPlayerViewController.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Application/Sources/Player/MediaPlayerViewController.m b/Application/Sources/Player/MediaPlayerViewController.m index 691e3e57a..6767ce848 100755 --- a/Application/Sources/Player/MediaPlayerViewController.m +++ b/Application/Sources/Player/MediaPlayerViewController.m @@ -1660,8 +1660,7 @@ - (void)letterboxView:(SRGLetterboxView *)letterboxView didSelectSubdivision:(SR SRGLetterboxController *letterboxController = letterboxView.controller; Playlist *playlist = [letterboxController.playlistDataSource isKindOfClass:Playlist.class] ? (Playlist *)letterboxController.playlistDataSource : nil; - NSString *keyPath = [NSString stringWithFormat:@"@distinctUnionOfObjects.%@", @keypath(SRGMedia.new, URN)]; - if (! [[playlist.medias valueForKeyPath:keyPath] containsObject:subdivision.URN]) { + if (! [[playlist.medias valueForKeyPath:@keypath(SRGMedia.new, URN)] containsObject:subdivision.URN]) { Playlist *playlist = PlaylistForURN(subdivision.URN); letterboxController.playlistDataSource = playlist; letterboxController.playbackTransitionDelegate = playlist; From 64ef304144a1c44983ee333fe79a3cf077177bc4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 18 Jul 2022 19:43:11 +0200 Subject: [PATCH 08/35] Prepare srg_app_store_connect_api_key to custom business unit --- fastlane/Fastfile | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5fe12dce9..05e12fab5 100755 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1148,14 +1148,21 @@ def appstore_beta_identifiers end end -def appstore_team_api_key_prefixes - { - ENV['RSI_ITUNES_CONNECT_TEAM_ID'] => 'RSI', - ENV['RTS_ITUNES_CONNECT_TEAM_ID'] => 'RTS', - ENV['SRF_ITUNES_CONNECT_TEAM_ID'] => 'SRF', - ENV['SRGSSR_ITUNES_CONNECT_TEAM_ID'] => 'SRGSSR', # RTR is in SRG SSR ASC team - ENV['SWI_ITUNES_CONNECT_TEAM_ID'] => 'SWI' - } +def itc_business_units + # RTR is in SRG SSR ASC team + business_units.map do |business_unit| + business_unit == 'RTR' ? 'SRGSSR' : business_unit + end +end + +def itc_team_ids + itc_business_units.map do |itc_business_unit| + ENV["#{itc_business_unit}_ITUNES_CONNECT_TEAM_ID"] + end +end + +def itc_team_id_index(itc_team_id) + itc_team_ids.index(itc_team_id) end # Returns current tag version @@ -1317,9 +1324,9 @@ def appstore_platform(platform) appstore_platforms[platform] end -def srg_app_store_connect_api_key - itc_team_id = app_config.try_fetch_value(:itc_team_id) - key_prefix = appstore_team_api_key_prefixes[itc_team_id] +def srg_app_store_connect_api_key(business_unit = nil) + business_unit ||= business_units[itc_team_id_index(app_config.try_fetch_value(:itc_team_id))] + key_prefix = itc_business_units[business_unit_index(business_unit)] return unless key_prefix folder_path = Dir.chdir('..') { Dir.pwd } From 086641f21d3b8265decae08b74119f9564a40de7 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 18 Jul 2022 20:10:18 +0200 Subject: [PATCH 09/35] Add lane to get release status --- fastlane/Fastfile | 52 +++++++++++++++++++++++++++++++++++++++++++++- fastlane/README.md | 8 +++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 05e12fab5..38fa160c8 100755 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -350,6 +350,34 @@ platform :ios do end end + desc 'Get AppStore release status for iOS and tvOS' + lane :appStoreReleaseStatus do + UI.message '-----' + business_units.map do |business_unit| + spaceship_login_with_api_key(business_unit) + app_identifier = appstore_build_identifiers[business_unit_index(business_unit)] + app = spaceship_app(app_identifier) + next unless app + + UI.message '-----' + ['iOS', 'tvOS'].map do |platform| + spaceship_platform = spaceship_platform(platform) + live_version = app.get_live_app_store_version(platform: spaceship_platform) + UI.success "Play #{business_unit} #{platform} live version #{live_version.version_string} is #{live_version.app_store_state}." if live_version + UI.important "Play #{business_unit} #{platform} has no live version." unless live_version + + latest_version = app.get_latest_app_store_version(platform: spaceship_platform) + if !latest_version || latest_version.version_string == live_version.version_string + UI.message "Play #{business_unit} #{platform} version #{latest_version.version_string} is the latest one." + else + UI.important "Play #{business_unit} #{platform} latest version #{latest_version.version_string} is #{latest_version.app_store_state}." + end + UI.message '-----' + end + end + UI.message '-----' + end + # Individual iOS screenshots desc 'RSI: Makes iOS screenshots and replaces current ones on App Store Connect.' @@ -1148,6 +1176,10 @@ def appstore_beta_identifiers end end +def appstore_build_identifiers + business_units.map { |business_unit| ENV["#{business_unit}_APP_IDENTIFIER"] } +end + def itc_business_units # RTR is in SRG SSR ASC team business_units.map do |business_unit| @@ -1624,7 +1656,7 @@ def crowdin_language(business_unit) end def pull_translations_lane_condition(lane) - lane.to_s.downcase.include? 'release' + lane.to_s.downcase.include? 'prepareappstorerelease' end def skip_pull_translations @@ -1713,6 +1745,24 @@ def spaceship_login Spaceship::Tunes.select_team end +def spaceship_login_with_api_key(business_unit = nil) + asc_api_key = srg_app_store_connect_api_key(business_unit) + return unless asc_api_key + + token = Spaceship::ConnectAPI::Token.create( + key_id: asc_api_key[:id], + issuer_id: asc_api_key[:issuerId], + filepath: asc_api_key[:filePath] + ) + + Spaceship::ConnectAPI.token = token +end + +def spaceship_platform(platform) + spaceship_platforms = { 'iOS' => 'IOS', 'tvOS' => 'TV_OS' } + spaceship_platforms[platform] +end + def spaceship_email_required(email) return if !email || (email.strip !~ URI::MailTo::EMAIL_REGEXP) diff --git a/fastlane/README.md b/fastlane/README.md index 8a86a549d..e8c275be3 100755 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -207,6 +207,14 @@ Sends latest tvOS App Store build dSYMs to App Center. Optional `build_number`, Prepare AppStore tvOS releases on App Store Connect with the current version and build numbers. No build uploads. Optional `tag_version` (`X.Y.Z-build_number`) or `submit_for_review` parameters. +### ios appStoreReleaseStatus + +```sh +[bundle exec] fastlane ios appStoreReleaseStatus +``` + +Get AppStore release status for iOS and tvOS + ### ios iOSrsiScreenshots ```sh From 1b373db47d1defef1b5a6395586de6e72e18c1b5 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 15 Aug 2022 20:57:40 +0200 Subject: [PATCH 10/35] Add build number information --- fastlane/Fastfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 38fa160c8..db4653246 100755 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -363,14 +363,15 @@ platform :ios do ['iOS', 'tvOS'].map do |platform| spaceship_platform = spaceship_platform(platform) live_version = app.get_live_app_store_version(platform: spaceship_platform) - UI.success "Play #{business_unit} #{platform} live version #{live_version.version_string} is #{live_version.app_store_state}." if live_version + UI.success "Play #{business_unit} #{platform} live version #{live_version.version_string} (#{live_version.build.version}) is #{live_version.app_store_state}." if live_version UI.important "Play #{business_unit} #{platform} has no live version." unless live_version latest_version = app.get_latest_app_store_version(platform: spaceship_platform) if !latest_version || latest_version.version_string == live_version.version_string - UI.message "Play #{business_unit} #{platform} version #{latest_version.version_string} is the latest one." + UI.message "Play #{business_unit} #{platform} version #{live_version.version_string} (#{live_version.build.version}) is the latest one." else - UI.important "Play #{business_unit} #{platform} latest version #{latest_version.version_string} is #{latest_version.app_store_state}." + build_version = latest_version.build ? latest_version.build.version : 'NaN' + UI.important "Play #{business_unit} #{platform} latest version #{latest_version.version_string} (#{build_version}) is #{latest_version.app_store_state}." end UI.message '-----' end From fa96f65f15ee381c27931ede8a0ef9cb962503a3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Tue, 23 Aug 2022 15:42:22 +0200 Subject: [PATCH 11/35] Update Firebase, Aiolos, AirShip and swift-protobuf dependencies --- ...om.mono0926.LicensePlist.latest_result.txt | 10 +++++----- .../com.mono0926.LicensePlist.plist | 10 +++++----- .../xcshareddata/swiftpm/Package.resolved | 20 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt index b6048e217..0c75b5eeb 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt @@ -105,7 +105,7 @@ version: 2.0.1 name: abseil-cpp-SwiftPM, nameSpecified: abseil-cpp-swiftpm, owner: firebase, version: 0.20220203.2, source: https://github.com/firebase/abseil-cpp-SwiftPM -name: Aiolos, nameSpecified: aiolos, owner: IdeasOnCanvas, version: 1.8.1, source: https://github.com/IdeasOnCanvas/Aiolos +name: Aiolos, nameSpecified: aiolos, owner: IdeasOnCanvas, version: 1.9.1, source: https://github.com/IdeasOnCanvas/Aiolos name: appcenter-sdk-apple, nameSpecified: appcenter-sdk-apple, owner: microsoft, version: 4.4.3, source: https://github.com/microsoft/appcenter-sdk-apple @@ -115,7 +115,7 @@ name: Comscore-Swift-Package-Manager, nameSpecified: comscore-swift-package-mana name: DZNEmptyDataSet, nameSpecified: dznemptydataset, owner: dzenbot, version: , source: https://github.com/dzenbot/DZNEmptyDataSet -name: firebase-ios-sdk, nameSpecified: firebase-ios-sdk, owner: firebase, version: 9.3.0, source: https://github.com/firebase/firebase-ios-sdk +name: firebase-ios-sdk, nameSpecified: firebase-ios-sdk, owner: firebase, version: 9.4.1, source: https://github.com/firebase/firebase-ios-sdk name: FSCalendar, nameSpecified: fscalendar, owner: WenchaoD, version: 2.8.4, source: https://github.com/WenchaoD/FSCalendar @@ -133,9 +133,9 @@ name: GoogleUtilities, nameSpecified: googleutilities, owner: google, version: 7 name: grpc-ios, nameSpecified: grpc-ios, owner: grpc, version: 1.44.3-grpc, source: https://github.com/grpc/grpc-ios -name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 2.0.0, source: https://github.com/google/gtm-session-fetcher +name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 2.1.0, source: https://github.com/google/gtm-session-fetcher -name: ios-library, nameSpecified: ios-library, owner: urbanairship, version: 16.8.0, source: https://github.com/urbanairship/ios-library +name: ios-library, nameSpecified: ios-library, owner: urbanairship, version: 16.9.2, source: https://github.com/urbanairship/ios-library name: leveldb, nameSpecified: leveldb, owner: firebase, version: 1.22.2, source: https://github.com/firebase/leveldb @@ -181,7 +181,7 @@ name: srguserdata-apple, nameSpecified: srguserdata-apple, owner: SRGSSR, versio name: swift-collections, nameSpecified: swift-collections, owner: apple, version: 1.0.2, source: https://github.com/apple/swift-collections -name: swift-protobuf, nameSpecified: swift-protobuf, owner: apple, version: 1.19.0, source: https://github.com/apple/swift-protobuf +name: swift-protobuf, nameSpecified: swift-protobuf, owner: apple, version: 1.19.1, source: https://github.com/apple/swift-protobuf name: SwiftMessages, nameSpecified: swiftmessages, owner: SwiftKickMobile, version: 9.0.6, source: https://github.com/SwiftKickMobile/SwiftMessages diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist index d11b30993..f13fe44f9 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist @@ -22,7 +22,7 @@ File com.mono0926.LicensePlist/Aiolos Title - aiolos (1.8.1) + aiolos (1.9.1) Type PSChildPaneSpecifier @@ -70,7 +70,7 @@ File com.mono0926.LicensePlist/firebase-ios-sdk Title - firebase-ios-sdk (9.3.0) + firebase-ios-sdk (9.4.1) Type PSChildPaneSpecifier @@ -142,7 +142,7 @@ File com.mono0926.LicensePlist/gtm-session-fetcher Title - gtm-session-fetcher (2.0.0) + gtm-session-fetcher (2.1.0) Type PSChildPaneSpecifier @@ -150,7 +150,7 @@ File com.mono0926.LicensePlist/ios-library Title - ios-library (16.8.0) + ios-library (16.9.2) Type PSChildPaneSpecifier @@ -358,7 +358,7 @@ File com.mono0926.LicensePlist/swift-protobuf Title - swift-protobuf (1.19.0) + swift-protobuf (1.19.1) Type PSChildPaneSpecifier diff --git a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved index f57bc45d2..2e5a80aa6 100644 --- a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/IdeasOnCanvas/Aiolos.git", "state" : { - "revision" : "1f38e1f0f3cebad4e92b5a9c34eca8e305754279", - "version" : "1.8.1" + "revision" : "fb9176dafaac5319a25bb2c48fbef1efcc1dc6da", + "version" : "1.9.1" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "8467858fc4cee858511380ac0f9ea5a17d007b5d", - "version" : "9.3.0" + "revision" : "cbaa7ce7063d5ab22414962ec63366ccbba26034", + "version" : "9.4.1" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "19605024d59eaefdb1f6a2cb11ebe75df4421126", - "version" : "2.0.0" + "revision" : "d4289da23e978f37c344ea6a386e5546e2466294", + "version" : "2.1.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/urbanairship/ios-library.git", "state" : { - "revision" : "c676b974f01773edd34449fafbcb84ea11fe4743", - "version" : "16.8.0" + "revision" : "be218058500206df98e082126b15728ce3d6b7d9", + "version" : "16.9.2" } }, { @@ -374,8 +374,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "e1499bc69b9040b29184f7f2996f7bab467c1639", - "version" : "1.19.0" + "revision" : "fa0fcd43f272a260e7f734f23e6dc55e16fcae0a", + "version" : "1.19.1" } }, { From ce4e7fc8032240802d8b53fd7c4b2509fefe5e8f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Wed, 24 Aug 2022 14:57:46 +0200 Subject: [PATCH 12/35] Update LB dependencies iOS timeline view was not interactive in some cases --- .../com.mono0926.LicensePlist.latest_result.txt | 2 +- .../Resources/Settings.bundle/com.mono0926.LicensePlist.plist | 2 +- PlaySRG.xcodeproj/project.pbxproj | 4 ++-- PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt index 0c75b5eeb..8aa113163 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt @@ -169,7 +169,7 @@ name: srgdiagnostics-apple, nameSpecified: srgdiagnostics-apple, owner: SRGSSR, name: srgidentity-apple, nameSpecified: srgidentity-apple, owner: SRGSSR, version: 3.3.0, source: https://github.com/SRGSSR/srgidentity-apple -name: srgletterbox-apple, nameSpecified: srgletterbox-apple, owner: SRGSSR, version: 8.3.0, source: https://github.com/SRGSSR/srgletterbox-apple +name: srgletterbox-apple, nameSpecified: srgletterbox-apple, owner: SRGSSR, version: , source: https://github.com/SRGSSR/srgletterbox-apple name: srglogger-apple, nameSpecified: srglogger-apple, owner: SRGSSR, version: 3.1.0, source: https://github.com/SRGSSR/srglogger-apple diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist index f13fe44f9..b541db406 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist @@ -310,7 +310,7 @@ File com.mono0926.LicensePlist/srgletterbox-apple Title - srgletterbox-apple (8.3.0) + srgletterbox-apple Type PSChildPaneSpecifier diff --git a/PlaySRG.xcodeproj/project.pbxproj b/PlaySRG.xcodeproj/project.pbxproj index 7abba51ae..e2d87a684 100644 --- a/PlaySRG.xcodeproj/project.pbxproj +++ b/PlaySRG.xcodeproj/project.pbxproj @@ -19054,8 +19054,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SRGSSR/srgletterbox-apple.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 8.3.0; + branch = develop; + kind = branch; }; }; 6F6DD3A924E449D7003F9437 /* XCRemoteSwiftPackageReference "srgdataprovider-apple" */ = { diff --git a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2e5a80aa6..54aaabd85 100644 --- a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -320,8 +320,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SRGSSR/srgletterbox-apple.git", "state" : { - "revision" : "2db25a0d0729b7f93c137906ea9f921a107d8334", - "version" : "8.3.0" + "branch" : "develop", + "revision" : "1c6545788da0158e26104025463858d629999aee" } }, { From 4f07ae9677f014668cafcbeecc2dafe34581f229 Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Fri, 26 Aug 2022 16:55:50 +0200 Subject: [PATCH 13/35] Create Letterbox controller only if necessary (#221) Don't create it in the storyboard --- .../Player/MediaPlayerViewController+Private.h | 2 +- .../Sources/Player/MediaPlayerViewController.m | 12 +++++++----- .../Player/MediaPlayerViewController.storyboard | 7 ++----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Application/Sources/Player/MediaPlayerViewController+Private.h b/Application/Sources/Player/MediaPlayerViewController+Private.h index 6d20d1aff..615bac3f8 100644 --- a/Application/Sources/Player/MediaPlayerViewController+Private.h +++ b/Application/Sources/Player/MediaPlayerViewController+Private.h @@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN @interface MediaPlayerViewController (Private) @property (nonatomic, readonly, weak) IBOutlet UITableView *programsTableView; -@property (nonatomic, readonly) IBOutlet SRGLetterboxController *letterboxController; +@property (nonatomic, readonly) SRGLetterboxController *letterboxController; @end diff --git a/Application/Sources/Player/MediaPlayerViewController.m b/Application/Sources/Player/MediaPlayerViewController.m index 6767ce848..6847971f7 100755 --- a/Application/Sources/Player/MediaPlayerViewController.m +++ b/Application/Sources/Player/MediaPlayerViewController.m @@ -97,7 +97,7 @@ @interface MediaPlayerViewController () @property (nonatomic) SRGLetterboxController *originalLetterboxController; // optional source controller, will be used if provided @property (nonatomic) SRGPosition *originalPosition; // original position to start at -@property (nonatomic) IBOutlet SRGLetterboxController *letterboxController; // top object, strong +@property (nonatomic) SRGLetterboxController *letterboxController; @property (nonatomic) SRGProgramComposition *programComposition; @property (nonatomic) NSArray *programs; @@ -236,6 +236,8 @@ - (instancetype)initWithURN:(NSString *)URN position:(SRGPosition *)position fro self.originalURN = URN; self.originalPosition = position; self.fromPushNotification = fromPushNotification; + + self.letterboxController = [[SRGLetterboxController alloc] init]; ApplicationConfigurationApplyControllerSettings(self.letterboxController); } return self; @@ -260,6 +262,8 @@ - (instancetype)initWithMedia:(SRGMedia *)media position:(SRGPosition *)position self.originalMedia = media; self.originalPosition = position; self.fromPushNotification = fromPushNotification; + + self.letterboxController = [[SRGLetterboxController alloc] init]; ApplicationConfigurationApplyControllerSettings(self.letterboxController); } return self; @@ -273,7 +277,6 @@ - (instancetype)initWithController:(SRGLetterboxController *)controller position self.originalPosition = position; self.fromPushNotification = fromPushNotification; - // Force the correct Letterbox controller. It will be linked to the Letterbox view in `-viewDidLoad` self.letterboxController = controller; } return self; @@ -351,6 +354,8 @@ - (void)viewDidLoad self.view.backgroundColor = UIColor.srg_gray16Color; + self.letterboxView.controller = self.letterboxController; + self.scrollView.hidden = YES; self.channelView.hidden = YES; @@ -425,10 +430,7 @@ - (void)viewDidLoad [self updateAvailabilityLabelHeight]; - // Use original controller, if any has been provided if (self.originalLetterboxController) { - self.letterboxView.controller = self.letterboxController; - // Always resume playback if the original controller was not playing [self.letterboxController play]; } diff --git a/Application/Sources/Player/MediaPlayerViewController.storyboard b/Application/Sources/Player/MediaPlayerViewController.storyboard index 132b71c4d..5de72885f 100755 --- a/Application/Sources/Player/MediaPlayerViewController.storyboard +++ b/Application/Sources/Player/MediaPlayerViewController.storyboard @@ -1,9 +1,9 @@ - + - + @@ -30,7 +30,6 @@ - @@ -670,7 +669,6 @@ - @@ -714,7 +712,6 @@ - From 435720322f09bb81c47e69ac2a6525f8988d7290 Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Fri, 26 Aug 2022 17:57:15 +0200 Subject: [PATCH 14/35] Expand text on tvOS show page (#223) --- .../Sources/Content/ShowHeaderView.swift | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/Application/Sources/Content/ShowHeaderView.swift b/Application/Sources/Content/ShowHeaderView.swift index 62bc245e7..050a6d628 100644 --- a/Application/Sources/Content/ShowHeaderView.swift +++ b/Application/Sources/Content/ShowHeaderView.swift @@ -74,6 +74,9 @@ struct ShowHeaderView: View { /// Behavior: h-hug, v-hug private struct DescriptionView: View { @ObservedObject var model: ShowHeaderViewModel +#if os(tvOS) + @State var isFocused = false +#endif var body: some View { VStack(spacing: ShowHeaderView.verticalSpacing) { @@ -90,13 +93,21 @@ struct ShowHeaderView: View { .multilineTextAlignment(.center) .foregroundColor(.srgGrayC7) if let lead = model.lead { - Text(lead) - .srgFont(.body) - .lineLimit(6) +#if os(iOS) + LeadView(lead) // See above .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.center) - .foregroundColor(.srgGray96) +#else + Button { + navigateToText(lead) + } label: { + LeadView(lead) + // See above + .fixedSize(horizontal: false, vertical: true) + .onParentFocusChange { isFocused = $0 } + } + .buttonStyle(TextButtonStyle(focused: isFocused)) +#endif } HStack(spacing: 20) { SimpleButton(icon: model.favoriteIcon, label: model.favoriteLabel, accessibilityLabel: model.favoriteAccessibilityLabel, action: favoriteAction) @@ -135,6 +146,23 @@ struct ShowHeaderView: View { model.toggleSubscription() } #endif + + /// Behavior: h-exp, v-hug + private struct LeadView: View { + let content: String + + var body: some View { + Text(content) + .srgFont(.body) + .lineLimit(6) + .multilineTextAlignment(.center) + .foregroundColor(.srgGray96) + } + + init(_ content: String) { + self.content = content + } + } } } From 698cf8985547a3af64814fa68c27831a1d7f35ca Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Tue, 30 Aug 2022 12:11:50 +0200 Subject: [PATCH 15/35] Avoid notification center publisher leaks (#219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Samuel Défago --- Application/Sources/CarPlay/CarPlayList.swift | 2 +- .../CarPlayTemplateListController.swift | 2 +- .../Sources/Content/PageViewController.swift | 2 +- Application/Sources/Content/Publishers.swift | 14 +++++++ .../Sources/Helpers/Accessibility.swift | 40 +++++++++---------- Application/Sources/Helpers/Signals.swift | 24 +++++------ .../Sources/Settings/SettingsViewModel.swift | 10 ++--- .../UI/Views/DownloadCellViewModel.swift | 4 +- TV Application/Sources/AppDelegate.swift | 6 +-- .../Sources/LetterboxDelegate.swift | 2 +- 10 files changed, 60 insertions(+), 46 deletions(-) diff --git a/Application/Sources/CarPlay/CarPlayList.swift b/Application/Sources/CarPlay/CarPlayList.swift index dfeb47c91..65e34b3fe 100644 --- a/Application/Sources/CarPlay/CarPlayList.swift +++ b/Application/Sources/CarPlay/CarPlayList.swift @@ -146,7 +146,7 @@ private extension CarPlayList { return SRGLetterboxService.shared.publisher(for: \.controller) .map { controller -> AnyPublisher in if let controller = controller { - return NotificationCenter.default.publisher(for: NSNotification.Name.SRGLetterboxMetadataDidChange, object: controller) + return NotificationCenter.default.weakPublisher(for: NSNotification.Name.SRGLetterboxMetadataDidChange, object: controller) .map { notification in let controller = notification.object as? SRGLetterboxController return nowPlayingMedia(for: controller) diff --git a/Application/Sources/CarPlay/CarPlayTemplateListController.swift b/Application/Sources/CarPlay/CarPlayTemplateListController.swift index e248e785c..a0dbe8b7e 100644 --- a/Application/Sources/CarPlay/CarPlayTemplateListController.swift +++ b/Application/Sources/CarPlay/CarPlayTemplateListController.swift @@ -54,7 +54,7 @@ final class CarPlayTemplateListController { } private static func foreground() -> AnyPublisher { - return NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification) + return NotificationCenter.default.weakPublisher(for: UIScene.willEnterForegroundNotification) .filter { $0.object is CPTemplateApplicationScene } .map { _ in } .eraseToAnyPublisher() diff --git a/Application/Sources/Content/PageViewController.swift b/Application/Sources/Content/PageViewController.swift index c85a6d5bf..00e4f1004 100644 --- a/Application/Sources/Content/PageViewController.swift +++ b/Application/Sources/Content/PageViewController.swift @@ -175,7 +175,7 @@ final class PageViewController: UIViewController { } .store(in: &cancellables) - NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) .sink { [weak self] _ in guard let self = self, self.play_isViewCurrent else { return } self.updateNavigationBar(animated: true) diff --git a/Application/Sources/Content/Publishers.swift b/Application/Sources/Content/Publishers.swift index d603eb900..7ef18b58b 100644 --- a/Application/Sources/Content/Publishers.swift +++ b/Application/Sources/Content/Publishers.swift @@ -9,6 +9,20 @@ import Combine import SRGDataProviderCombine import SRGUserData +extension NotificationCenter { + /// The usual notification publisher retains the filter object, potentially creating cycles. Apply filter on + /// unfiltered stream to avoid this issue. + func weakPublisher(for name: Notification.Name, object: AnyObject? = nil) -> AnyPublisher { + publisher(for: name) + .filter { [weak object] notification in + guard let object = object else { return true } + guard let notificationObject = notification.object as? AnyObject else { return false } + return notificationObject === object + } + .eraseToAnyPublisher() + } +} + extension SRGDataProvider { /// Publishes the latest 30 episodes for a show URN list. func latestMediasForShowsPublisher(withUrns urns: [String], pageSize: UInt = SRGDataProviderDefaultPageSize) -> AnyPublisher<[SRGMedia], Error> { diff --git a/Application/Sources/Helpers/Accessibility.swift b/Application/Sources/Helpers/Accessibility.swift index bcf3d2e76..a3e6bf408 100644 --- a/Application/Sources/Helpers/Accessibility.swift +++ b/Application/Sources/Helpers/Accessibility.swift @@ -52,64 +52,64 @@ final class AccessibilitySettings: ObservableObject { @Published var isOnOffSwitchLabelsEnabled = UIAccessibility.isOnOffSwitchLabelsEnabled private init() { - NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) .map { _ in UIAccessibility.isVoiceOverRunning } .assign(to: &$isVoiceOverRunning) - NotificationCenter.default.publisher(for: UIAccessibility.monoAudioStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.monoAudioStatusDidChangeNotification) .map { _ in UIAccessibility.isMonoAudioEnabled } .assign(to: &$isMonoAudioEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.closedCaptioningStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.closedCaptioningStatusDidChangeNotification) .map { _ in UIAccessibility.isClosedCaptioningEnabled } .assign(to: &$isClosedCaptioningEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.invertColorsStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.invertColorsStatusDidChangeNotification) .map { _ in UIAccessibility.isInvertColorsEnabled } .assign(to: &$isInvertColorsEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.guidedAccessStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.guidedAccessStatusDidChangeNotification) .map { _ in UIAccessibility.isGuidedAccessEnabled } .assign(to: &$isGuidedAccessEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.boldTextStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.boldTextStatusDidChangeNotification) .map { _ in UIAccessibility.isBoldTextEnabled } .assign(to: &$isBoldTextEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.buttonShapesEnabledStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.buttonShapesEnabledStatusDidChangeNotification) .map { _ in UIAccessibility.buttonShapesEnabled } .assign(to: &$buttonShapesEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.grayscaleStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.grayscaleStatusDidChangeNotification) .map { _ in UIAccessibility.isGrayscaleEnabled } .assign(to: &$isGrayscaleEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.reduceTransparencyStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.reduceTransparencyStatusDidChangeNotification) .map { _ in UIAccessibility.isReduceTransparencyEnabled } .assign(to: &$isReduceTransparencyEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.reduceMotionStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.reduceMotionStatusDidChangeNotification) .map { _ in UIAccessibility.isReduceMotionEnabled } .assign(to: &$isReduceMotionEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.prefersCrossFadeTransitionsStatusDidChange) + NotificationCenter.default.weakPublisher(for: UIAccessibility.prefersCrossFadeTransitionsStatusDidChange) .map { _ in UIAccessibility.prefersCrossFadeTransitions } .assign(to: &$prefersCrossFadeTransitions) - NotificationCenter.default.publisher(for: UIAccessibility.videoAutoplayStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.videoAutoplayStatusDidChangeNotification) .map { _ in UIAccessibility.isVideoAutoplayEnabled } .assign(to: &$isVideoAutoplayEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.darkerSystemColorsStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.darkerSystemColorsStatusDidChangeNotification) .map { _ in UIAccessibility.isDarkerSystemColorsEnabled } .assign(to: &$isDarkerSystemColorsEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.switchControlStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.switchControlStatusDidChangeNotification) .map { _ in UIAccessibility.isSwitchControlRunning } .assign(to: &$isSwitchControlRunning) - NotificationCenter.default.publisher(for: UIAccessibility.speakSelectionStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.speakSelectionStatusDidChangeNotification) .map { _ in UIAccessibility.isSpeakSelectionEnabled } .assign(to: &$isSpeakSelectionEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.speakScreenStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.speakScreenStatusDidChangeNotification) .map { _ in UIAccessibility.isSpeakScreenEnabled } .assign(to: &$isSpeakScreenEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.shakeToUndoDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.shakeToUndoDidChangeNotification) .map { _ in UIAccessibility.isShakeToUndoEnabled } .assign(to: &$isShakeToUndoEnabled) - NotificationCenter.default.publisher(for: UIAccessibility.assistiveTouchStatusDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.assistiveTouchStatusDidChangeNotification) .map { _ in UIAccessibility.isAssistiveTouchRunning } .assign(to: &$isAssistiveTouchRunning) - NotificationCenter.default.publisher(for: UIAccessibility.differentiateWithoutColorDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.differentiateWithoutColorDidChangeNotification) .map { _ in UIAccessibility.shouldDifferentiateWithoutColor } .assign(to: &$shouldDifferentiateWithoutColor) - NotificationCenter.default.publisher(for: UIAccessibility.onOffSwitchLabelsDidChangeNotification) + NotificationCenter.default.weakPublisher(for: UIAccessibility.onOffSwitchLabelsDidChangeNotification) .map { _ in UIAccessibility.isOnOffSwitchLabelsEnabled } .assign(to: &$isOnOffSwitchLabelsEnabled) } diff --git a/Application/Sources/Helpers/Signals.swift b/Application/Sources/Helpers/Signals.swift index cb5866e35..a706ae021 100644 --- a/Application/Sources/Helpers/Signals.swift +++ b/Application/Sources/Helpers/Signals.swift @@ -40,7 +40,7 @@ enum ThrottledSignal { * Emits a signal when the history is updated for some uid or, if omitted, when any history update occurs. */ static func historyUpdates(for uid: String? = nil, interval: TimeInterval = 10) -> AnyPublisher { - return NotificationCenter.default.publisher(for: .SRGHistoryEntriesDidChange, object: SRGUserData.current?.history) + return NotificationCenter.default.weakPublisher(for: .SRGHistoryEntriesDidChange, object: SRGUserData.current?.history) .filter { notification in guard let uid = uid else { return true } if let updatedUids = notification.userInfo?[SRGHistoryEntriesUidsKey] as? Set, updatedUids.contains(uid) { @@ -59,7 +59,7 @@ enum ThrottledSignal { * Emits a signal when the watch later playlist is updated for some uid or, if omitted, when any watch later update occurs. */ static func watchLaterUpdates(for uid: String? = nil, interval: TimeInterval = 10) -> AnyPublisher { - return NotificationCenter.default.publisher(for: .SRGPlaylistEntriesDidChange, object: SRGUserData.current?.playlists) + return NotificationCenter.default.weakPublisher(for: .SRGPlaylistEntriesDidChange, object: SRGUserData.current?.playlists) .filter { notification in if let playlistUid = notification.userInfo?[SRGPlaylistUidKey] as? String, playlistUid == SRGPlaylistUid.watchLater.rawValue { guard let uid = uid else { return true } @@ -83,7 +83,7 @@ enum ThrottledSignal { * Emits a signal when the user preferences are updated. */ static func preferenceUpdates(interval: TimeInterval = 10) -> AnyPublisher { - return NotificationCenter.default.publisher(for: .SRGPreferencesDidChange, object: SRGUserData.current?.preferences) + return NotificationCenter.default.weakPublisher(for: .SRGPreferencesDidChange, object: SRGUserData.current?.preferences) .filter { notification in if let domains = notification.userInfo?[SRGPreferencesDomainsKey] as? Set, domains.contains(PlayPreferencesDomain) { return true @@ -102,7 +102,7 @@ enum ThrottledSignal { * Emits a signal when downloads are updated. */ static func downloadUpdates(interval: TimeInterval = 10) -> AnyPublisher { - return NotificationCenter.default.publisher(for: .DownloadStateDidChange, object: nil) + return NotificationCenter.default.weakPublisher(for: .DownloadStateDidChange, object: nil) .throttle(for: .seconds(interval), scheduler: DispatchQueue.main, latest: true) .map { _ in } .eraseToAnyPublisher() @@ -125,7 +125,7 @@ enum ApplicationSignal { * Emits a signal when the network is reachable again. */ static func reachable() -> AnyPublisher { - return NotificationCenter.default.publisher(for: .FXReachabilityStatusDidChange) + return NotificationCenter.default.weakPublisher(for: .FXReachabilityStatusDidChange) .filter { ReachabilityBecameReachable($0) } .map { _ in } .eraseToAnyPublisher() @@ -135,7 +135,7 @@ enum ApplicationSignal { * Emits a signal when the application moves to the foreground. */ static func foreground() -> AnyPublisher { - return NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) + return NotificationCenter.default.weakPublisher(for: UIApplication.willEnterForegroundNotification) .map { _ in } .eraseToAnyPublisher() } @@ -144,7 +144,7 @@ enum ApplicationSignal { /// push notifications static func pushServiceStatusUpdate() -> AnyPublisher { #if os(iOS) - return NotificationCenter.default.publisher(for: .PushServiceStatusDidChange) + return NotificationCenter.default.weakPublisher(for: .PushServiceStatusDidChange) .map { _ in } .eraseToAnyPublisher() #else @@ -204,7 +204,7 @@ enum UserInteractionSignal { #if os(iOS) static func downloadUpdates() -> AnyPublisher<[Content.Item], Never> { - return NotificationCenter.default.publisher(for: .didUpdateDownloads) + return NotificationCenter.default.weakPublisher(for: .didUpdateDownloads) .scan([Content.Item]()) { consolidate(items: $0, with: $1) } .removeDuplicates() .eraseToAnyPublisher() @@ -212,14 +212,14 @@ enum UserInteractionSignal { #endif static func favoriteUpdates() -> AnyPublisher<[Content.Item], Never> { - return NotificationCenter.default.publisher(for: .didUpdateFavorites) + return NotificationCenter.default.weakPublisher(for: .didUpdateFavorites) .scan([Content.Item]()) { consolidate(items: $0, with: $1) } .removeDuplicates() .eraseToAnyPublisher() } static func historyUpdates() -> AnyPublisher<[Content.Item], Never> { - return NotificationCenter.default.publisher(for: .didUpdateHistoryEntries) + return NotificationCenter.default.weakPublisher(for: .didUpdateHistoryEntries) .scan([Content.Item]()) { consolidate(items: $0, with: $1) } .removeDuplicates() .eraseToAnyPublisher() @@ -227,7 +227,7 @@ enum UserInteractionSignal { #if os(iOS) static func notificationUpdates() -> AnyPublisher<[Content.Item], Never> { - return NotificationCenter.default.publisher(for: .didUpdateNotifications) + return NotificationCenter.default.weakPublisher(for: .didUpdateNotifications) .scan([Content.Item]()) { consolidate(items: $0, with: $1) } .removeDuplicates() .eraseToAnyPublisher() @@ -235,7 +235,7 @@ enum UserInteractionSignal { #endif static func watchLaterUpdates() -> AnyPublisher<[Content.Item], Never> { - return NotificationCenter.default.publisher(for: .didUpdateWatchLaterEntries) + return NotificationCenter.default.weakPublisher(for: .didUpdateWatchLaterEntries) .scan([Content.Item]()) { consolidate(items: $0, with: $1) } .removeDuplicates() .eraseToAnyPublisher() diff --git a/Application/Sources/Settings/SettingsViewModel.swift b/Application/Sources/Settings/SettingsViewModel.swift index 4b25d4fee..dd1e96f8e 100644 --- a/Application/Sources/Settings/SettingsViewModel.swift +++ b/Application/Sources/Settings/SettingsViewModel.swift @@ -22,7 +22,7 @@ final class SettingsViewModel: ObservableObject { @Published private var synchronizationDate: Date? init() { - NotificationCenter.default.publisher(for: .SRGUserDataDidFinishSynchronization, object: SRGUserData.current) + NotificationCenter.default.weakPublisher(for: .SRGUserDataDidFinishSynchronization, object: SRGUserData.current) .map { _ in } .prepend(()) .map { SRGUserData.current!.user.synchronizationDate } @@ -35,7 +35,7 @@ final class SettingsViewModel: ObservableObject { .assign(to: &$isLoggedIn) #if os(tvOS) - NotificationCenter.default.publisher(for: .SRGIdentityServiceDidUpdateAccount, object: identityService) + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceDidUpdateAccount, object: identityService) .map { _ in } .prepend(()) .map { identityService.account } @@ -75,9 +75,9 @@ final class SettingsViewModel: ObservableObject { private static func loggedInReloadSignal(for identityService: SRGIdentityService) -> AnyPublisher { return Publishers.Merge3( - NotificationCenter.default.publisher(for: .SRGIdentityServiceUserDidCancelLogin, object: identityService), - NotificationCenter.default.publisher(for: .SRGIdentityServiceUserDidLogin, object: identityService), - NotificationCenter.default.publisher(for: .SRGIdentityServiceUserDidLogout, object: identityService) + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceUserDidCancelLogin, object: identityService), + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceUserDidLogin, object: identityService), + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceUserDidLogout, object: identityService) ) .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) .map { _ in } diff --git a/Application/Sources/UI/Views/DownloadCellViewModel.swift b/Application/Sources/UI/Views/DownloadCellViewModel.swift index 2364a0a7c..b456da765 100644 --- a/Application/Sources/UI/Views/DownloadCellViewModel.swift +++ b/Application/Sources/UI/Views/DownloadCellViewModel.swift @@ -30,10 +30,10 @@ final class DownloadCellViewModel: ObservableObject { $download .map { download in return Publishers.Merge( - NotificationCenter.default.publisher(for: NSNotification.Name.DownloadStateDidChange, object: download) + NotificationCenter.default.weakPublisher(for: NSNotification.Name.DownloadStateDidChange, object: download) .compactMap { $0.userInfo?[DownloadStateKey] as? Int } .map { Self.state(from: DownloadState(rawValue: $0), for: download) }, - NotificationCenter.default.publisher(for: NSNotification.Name.DownloadProgressDidChange, object: download) + NotificationCenter.default.weakPublisher(for: NSNotification.Name.DownloadProgressDidChange, object: download) .compactMap { $0.userInfo?[DownloadProgressKey] as? Progress } .map { State.downloading(progress: $0) } ) diff --git a/TV Application/Sources/AppDelegate.swift b/TV Application/Sources/AppDelegate.swift index 8e71131f7..a0a1938b0 100644 --- a/TV Application/Sources/AppDelegate.swift +++ b/TV Application/Sources/AppDelegate.swift @@ -60,7 +60,7 @@ extension AppDelegate: UIApplicationDelegate { let identityWebsiteURL = configuration.identityWebsiteURL { SRGIdentityService.current = SRGIdentityService(webserviceURL: identityWebserviceURL, websiteURL: identityWebsiteURL) - NotificationCenter.default.publisher(for: .SRGIdentityServiceUserDidCancelLogin, object: SRGIdentityService.current) + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceUserDidCancelLogin, object: SRGIdentityService.current) .sink { _ in let labels = SRGAnalyticsHiddenEventLabels() labels.source = AnalyticsSource.button.rawValue @@ -69,7 +69,7 @@ extension AppDelegate: UIApplicationDelegate { } .store(in: &cancellables) - NotificationCenter.default.publisher(for: .SRGIdentityServiceUserDidLogin, object: SRGIdentityService.current) + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceUserDidLogin, object: SRGIdentityService.current) .sink { _ in let labels = SRGAnalyticsHiddenEventLabels() labels.source = AnalyticsSource.button.rawValue @@ -78,7 +78,7 @@ extension AppDelegate: UIApplicationDelegate { } .store(in: &cancellables) - NotificationCenter.default.publisher(for: .SRGIdentityServiceUserDidLogout, object: SRGIdentityService.current) + NotificationCenter.default.weakPublisher(for: .SRGIdentityServiceUserDidLogout, object: SRGIdentityService.current) .sink { notification in let unexpectedLogout = notification.userInfo?[SRGIdentityServiceUnauthorizedKey] as? Bool ?? false diff --git a/TV Application/Sources/LetterboxDelegate.swift b/TV Application/Sources/LetterboxDelegate.swift index fea504c5b..ab2c26976 100644 --- a/TV Application/Sources/LetterboxDelegate.swift +++ b/TV Application/Sources/LetterboxDelegate.swift @@ -13,7 +13,7 @@ class LetterboxDelegate: NSObject { var cancellables = Set() override init() { - NotificationCenter.default.publisher(for: .SRGLetterboxPlaybackDidContinueAutomatically) + NotificationCenter.default.weakPublisher(for: .SRGLetterboxPlaybackDidContinueAutomatically) .sink { notification in guard let media = notification.userInfo?[SRGLetterboxMediaKey] as? SRGMedia else { return } From 87cbd97abdf6360c8f8d1cdc8bbb2982fd07c0ce Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Tue, 30 Aug 2022 12:49:38 +0200 Subject: [PATCH 16/35] Keep player alive enough in background to play next content (#228) --- Application/Sources/Application/AppDelegate.m | 1 + .../Sources/Helpers/RemoteCommandCenter.swift | 19 +++++++++++++++++++ PlaySRG.xcodeproj/project.pbxproj | 12 ++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 Application/Sources/Helpers/RemoteCommandCenter.swift diff --git a/Application/Sources/Application/AppDelegate.m b/Application/Sources/Application/AppDelegate.m index 3dbbe5d2b..122c1c555 100755 --- a/Application/Sources/Application/AppDelegate.m +++ b/Application/Sources/Application/AppDelegate.m @@ -51,6 +51,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( NSAssert(NSClassFromString(@"ASIdentifierManager") == Nil, @"No implicit AdSupport.framework dependency must be found"); [AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayback error:NULL]; + [RemoteCommandCenter activateRatingCommand]; PlayApplicationRunOnce(^(void (^completionHandler)(BOOL success)) { [PlayFirebaseConfiguration clearFirebaseConfigurationCache]; diff --git a/Application/Sources/Helpers/RemoteCommandCenter.swift b/Application/Sources/Helpers/RemoteCommandCenter.swift new file mode 100644 index 000000000..24253293f --- /dev/null +++ b/Application/Sources/Helpers/RemoteCommandCenter.swift @@ -0,0 +1,19 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation +import MediaPlayer + +// Magic subscription handler to presumably make iOS keep the player alive so we can continuously play audio in the background. +final class RemoteCommandCenter: NSObject { + @objc static func activateRatingCommand() { + MPRemoteCommandCenter.shared().ratingCommand.addTarget(self, action: #selector(Self.doNothing)) + } + + @objc private static func doNothing() -> MPRemoteCommandHandlerStatus { + .success + } +} diff --git a/PlaySRG.xcodeproj/project.pbxproj b/PlaySRG.xcodeproj/project.pbxproj index e2d87a684..62f9fafd3 100644 --- a/PlaySRG.xcodeproj/project.pbxproj +++ b/PlaySRG.xcodeproj/project.pbxproj @@ -57,6 +57,11 @@ 04D5F926286C4542000A5A4E /* Recommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D5F91E286C4542000A5A4E /* Recommendation.swift */; }; 04D5F927286C4542000A5A4E /* Recommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D5F91E286C4542000A5A4E /* Recommendation.swift */; }; 04D5F928286C4542000A5A4E /* Recommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D5F91E286C4542000A5A4E /* Recommendation.swift */; }; + 04E031CF28BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */; }; + 04E031D028BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */; }; + 04E031D128BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */; }; + 04E031D228BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */; }; + 04E031D328BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */; }; 04E4DEEB283678C900698BF8 /* ServiceMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E4DEEA283678C900698BF8 /* ServiceMessage.swift */; }; 04E4DEEC283678C900698BF8 /* ServiceMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E4DEEA283678C900698BF8 /* ServiceMessage.swift */; }; 04E4DEED283678C900698BF8 /* ServiceMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E4DEEA283678C900698BF8 /* ServiceMessage.swift */; }; @@ -2672,6 +2677,7 @@ 0451ECE228742D8000E89975 /* UIWindowScene+PlaySRG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowScene+PlaySRG.swift"; sourceTree = ""; }; 04D5477C27BFFE79003D1BC2 /* LoadingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingCell.swift; sourceTree = ""; }; 04D5F91E286C4542000A5A4E /* Recommendation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recommendation.swift; sourceTree = ""; }; + 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteCommandCenter.swift; sourceTree = ""; }; 04E4DEEA283678C900698BF8 /* ServiceMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceMessage.swift; sourceTree = ""; }; 05F9A586175D559FBBCA636E /* Pods-Play SRG-iOS-Play RTS.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Play SRG-iOS-Play RTS.appstore.xcconfig"; path = "Pods/Target Support Files/Pods-Play SRG-iOS-Play RTS/Pods-Play SRG-iOS-Play RTS.appstore.xcconfig"; sourceTree = ""; }; 08013EB325DC28F60099A6E8 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS14.3.sdk/System/Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; @@ -3852,6 +3858,7 @@ 08BDB5B1228D520000335898 /* PushService+Private.h */, 6F0850F226256A7700B4E410 /* Reachability.h */, 6F0850F326256A7700B4E410 /* Reachability.m */, + 04E031CE28BD0EF000450D38 /* RemoteCommandCenter.swift */, 6F978B4F2849C3CA003061E8 /* ScrollableContent.h */, 6F978B502849C3CA003061E8 /* ScrollableContent.m */, 08AF947E217D27E40028B082 /* SharingItem.h */, @@ -8472,6 +8479,7 @@ 6F58903426AED4CD00553C24 /* Environment.swift in Sources */, 6FCA5BD627D9DE0900916D0B /* DiskInfoFooterView.swift in Sources */, 6FB899FF26335B090012F1B0 /* Stack.swift in Sources */, + 04E031CF28BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */, 6FD4C2D6268B6CBB00F06F63 /* SimpleButton.swift in Sources */, 6F16C80C26025945006F685A /* PageViewModel.swift in Sources */, 08F5DB10262DC7F700F717D0 /* Logger.swift in Sources */, @@ -8505,6 +8513,7 @@ 6F6E9062283E6EB10049FEEE /* Highlight.swift in Sources */, 6F2F556C27B40967003DC9C0 /* NowArrowView.swift in Sources */, E66BEC1D1DA7FCED00AD4450 /* MediaPlayerViewController.m in Sources */, + 04E031D028BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */, 6F2DE05426440FB10059335E /* ActivityIndicator.swift in Sources */, 0821D53F227F5ED300FC091F /* DeepLinkService.m in Sources */, 6FD4C2D7268B6CBB00F06F63 /* SimpleButton.swift in Sources */, @@ -8754,6 +8763,7 @@ 6F6E9063283E6EB10049FEEE /* Highlight.swift in Sources */, 6F2F556D27B40967003DC9C0 /* NowArrowView.swift in Sources */, E66BEC1E1DA7FCED00AD4450 /* MediaPlayerViewController.m in Sources */, + 04E031D128BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */, 6F2DE05526440FB10059335E /* ActivityIndicator.swift in Sources */, 0821D540227F5ED300FC091F /* DeepLinkService.m in Sources */, 6FD4C2D8268B6CBB00F06F63 /* SimpleButton.swift in Sources */, @@ -9003,6 +9013,7 @@ 6F6E9064283E6EB10049FEEE /* Highlight.swift in Sources */, 6F2F556E27B40967003DC9C0 /* NowArrowView.swift in Sources */, E66BEC1F1DA7FCED00AD4450 /* MediaPlayerViewController.m in Sources */, + 04E031D228BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */, 6F2DE05626440FB20059335E /* ActivityIndicator.swift in Sources */, 0821D541227F5ED300FC091F /* DeepLinkService.m in Sources */, 6FD4C2D9268B6CBB00F06F63 /* SimpleButton.swift in Sources */, @@ -9313,6 +9324,7 @@ 6FD633E21FFBC0BE00875BE5 /* GoogleCastMiniPlayerView.m in Sources */, 6FB9B3DA26CA88AD0065092F /* TransluscentHeaderView.swift in Sources */, 6F56F9F8247C407100B2387B /* ChannelServiceSetup.m in Sources */, + 04E031D328BD0EF000450D38 /* RemoteCommandCenter.swift in Sources */, 6FC0C699245FF06D00B44CAE /* ProgramHeaderView.m in Sources */, 6F010E15286607450024A745 /* SearchSettingsBucketCell.swift in Sources */, 6F33E6172860AF0200724E76 /* Navigation.swift in Sources */, From a67aeeee5d60f22bcdfda10f2a95cb0314d6b5ca Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Tue, 30 Aug 2022 13:00:43 +0200 Subject: [PATCH 17/35] Set recommended playlist to on demand content played with CarPlay (#226) --- Application/Sources/Bridges/PlaySRG-ObjectiveC.h | 1 + Application/Sources/CarPlay/CarPlay+Extensions.swift | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Application/Sources/Bridges/PlaySRG-ObjectiveC.h b/Application/Sources/Bridges/PlaySRG-ObjectiveC.h index da78c7674..6aa331849 100755 --- a/Application/Sources/Bridges/PlaySRG-ObjectiveC.h +++ b/Application/Sources/Bridges/PlaySRG-ObjectiveC.h @@ -31,6 +31,7 @@ #import "PlayApplicationNavigation.h" #import "PlayDurationFormatter.h" #import "PlayErrors.h" +#import "Playlist.h" #import "PushService.h" #import "Reachability.h" #import "RefreshControl.h" diff --git a/Application/Sources/CarPlay/CarPlay+Extensions.swift b/Application/Sources/CarPlay/CarPlay+Extensions.swift index c915c8dab..031397cc9 100644 --- a/Application/Sources/CarPlay/CarPlay+Extensions.swift +++ b/Application/Sources/CarPlay/CarPlay+Extensions.swift @@ -31,6 +31,12 @@ extension CPInterfaceController { SRGLetterboxService.shared.enable(with: controller, pictureInPictureDelegate: nil) } + if let controller = SRGLetterboxService.shared.controller { + let playlist = PlaylistForURN(media.urn) + controller.playlistDataSource = playlist + controller.playbackTransitionDelegate = playlist + } + let nowPlayingTemplate = CPNowPlayingTemplate.shared pushTemplate(nowPlayingTemplate, animated: true) { _, _ in completion() From a233b009f68adbc2ebeee63c33b31edba6953c3f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Tue, 30 Aug 2022 13:11:35 +0200 Subject: [PATCH 18/35] Update Firebase and swift-protobuf dependencies --- .../com.mono0926.LicensePlist.latest_result.txt | 6 +++--- .../Settings.bundle/com.mono0926.LicensePlist.plist | 6 +++--- .../xcshareddata/swiftpm/Package.resolved | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt index 8aa113163..2e6925967 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt @@ -115,7 +115,7 @@ name: Comscore-Swift-Package-Manager, nameSpecified: comscore-swift-package-mana name: DZNEmptyDataSet, nameSpecified: dznemptydataset, owner: dzenbot, version: , source: https://github.com/dzenbot/DZNEmptyDataSet -name: firebase-ios-sdk, nameSpecified: firebase-ios-sdk, owner: firebase, version: 9.4.1, source: https://github.com/firebase/firebase-ios-sdk +name: firebase-ios-sdk, nameSpecified: firebase-ios-sdk, owner: firebase, version: 9.5.0, source: https://github.com/firebase/firebase-ios-sdk name: FSCalendar, nameSpecified: fscalendar, owner: WenchaoD, version: 2.8.4, source: https://github.com/WenchaoD/FSCalendar @@ -133,7 +133,7 @@ name: GoogleUtilities, nameSpecified: googleutilities, owner: google, version: 7 name: grpc-ios, nameSpecified: grpc-ios, owner: grpc, version: 1.44.3-grpc, source: https://github.com/grpc/grpc-ios -name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 2.1.0, source: https://github.com/google/gtm-session-fetcher +name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 2.0.0, source: https://github.com/google/gtm-session-fetcher name: ios-library, nameSpecified: ios-library, owner: urbanairship, version: 16.9.2, source: https://github.com/urbanairship/ios-library @@ -181,7 +181,7 @@ name: srguserdata-apple, nameSpecified: srguserdata-apple, owner: SRGSSR, versio name: swift-collections, nameSpecified: swift-collections, owner: apple, version: 1.0.2, source: https://github.com/apple/swift-collections -name: swift-protobuf, nameSpecified: swift-protobuf, owner: apple, version: 1.19.1, source: https://github.com/apple/swift-protobuf +name: swift-protobuf, nameSpecified: swift-protobuf, owner: apple, version: 1.20.1, source: https://github.com/apple/swift-protobuf name: SwiftMessages, nameSpecified: swiftmessages, owner: SwiftKickMobile, version: 9.0.6, source: https://github.com/SwiftKickMobile/SwiftMessages diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist index b541db406..492b75978 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist @@ -70,7 +70,7 @@ File com.mono0926.LicensePlist/firebase-ios-sdk Title - firebase-ios-sdk (9.4.1) + firebase-ios-sdk (9.5.0) Type PSChildPaneSpecifier @@ -142,7 +142,7 @@ File com.mono0926.LicensePlist/gtm-session-fetcher Title - gtm-session-fetcher (2.1.0) + gtm-session-fetcher (2.0.0) Type PSChildPaneSpecifier @@ -358,7 +358,7 @@ File com.mono0926.LicensePlist/swift-protobuf Title - swift-protobuf (1.19.1) + swift-protobuf (1.20.1) Type PSChildPaneSpecifier diff --git a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved index 54aaabd85..ad96c0dec 100644 --- a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "cbaa7ce7063d5ab22414962ec63366ccbba26034", - "version" : "9.4.1" + "revision" : "7f31a43f8c49bd4a1723bc9fecdfaa4411dd9f36", + "version" : "9.5.0" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "d4289da23e978f37c344ea6a386e5546e2466294", - "version" : "2.1.0" + "revision" : "19605024d59eaefdb1f6a2cb11ebe75df4421126", + "version" : "2.0.0" } }, { @@ -374,8 +374,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "fa0fcd43f272a260e7f734f23e6dc55e16fcae0a", - "version" : "1.19.1" + "revision" : "b8230909dedc640294d7324d37f4c91ad3dcf177", + "version" : "1.20.1" } }, { From ead01dc1090d801fd2ca2b56adcc63269cfccbfe Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Tue, 30 Aug 2022 16:27:25 +0200 Subject: [PATCH 19/35] Update what's new --- WhatsNew-iOS-beta.json | 3 ++- WhatsNew-tvOS-beta.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WhatsNew-iOS-beta.json b/WhatsNew-iOS-beta.json index 8e1f55cc8..907ce9797 100755 --- a/WhatsNew-iOS-beta.json +++ b/WhatsNew-iOS-beta.json @@ -169,5 +169,6 @@ "3.6.3-390": "- Adopt a modern look & feel for iOS 15 (large titles and scroll edge appearance).\n- Update application settings view.\n- Update search filters view.", "3.6.3-391": "- The search view now presents topics in addition to the most searched shows.\n- Home page hero stage and highlights should have a better image framing.\n- Improve VoiceOver nagivation by disabling UI large titles.", "3.6.4-392": "- The audio track and subtitle selection window can rotate on iPhone.", - "3.6.4-393": "- Fix inverted favorite & subscription banner messages.\n- Remove superfluous pinch gestures on player if device screen ratio fit content image ratio." + "3.6.4-393": "- Fix inverted favorite & subscription banner messages.\n- Remove superfluous pinch gestures on player if device screen ratio fit content image ratio.", + "3.6.5-394": "- Update next recommended media if the user changed the suject in the player view.\n- Add next recommended audio for CarPlay usage.\n- Fix player timeline clickable when playback stopped.\n- Fix player timeline clickable on Mac with Apple silicon." } \ No newline at end of file diff --git a/WhatsNew-tvOS-beta.json b/WhatsNew-tvOS-beta.json index 8e1fba0d2..e6117a3ba 100755 --- a/WhatsNew-tvOS-beta.json +++ b/WhatsNew-tvOS-beta.json @@ -46,5 +46,6 @@ "1.6.3-45": "- The search view now presents topics in addition to the most searched shows.\n- Home page hero stage and highlights should have a better image framing.", "1.6.3-46": "- Support informations can be displayed in profile view.", "1.6.4-47": "- Fix user interface glitches when navigating from the player to the media detail view.\n- Fix page layout on tvOS 16.", - "1.6.4-48": "- Prepare AppStore release." + "1.6.4-48": "- Prepare AppStore release.", + "1.6.5-49": "- Allow to expand text on show page." } \ No newline at end of file From 04b47a95188a7f2ce0bcd49886ed0106eb1e7c58 Mon Sep 17 00:00:00 2001 From: RTS Devops Date: Tue, 30 Aug 2022 17:29:09 +0200 Subject: [PATCH 20/35] Bump tvOS build number to 50 --- Xcode/Shared/Targets/tvOS/Common.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Shared/Targets/tvOS/Common.xcconfig b/Xcode/Shared/Targets/tvOS/Common.xcconfig index 93be910c1..f5078b1dc 100755 --- a/Xcode/Shared/Targets/tvOS/Common.xcconfig +++ b/Xcode/Shared/Targets/tvOS/Common.xcconfig @@ -2,7 +2,7 @@ // Version information MARKETING_VERSION = 1.6.5 -CURRENT_PROJECT_VERSION = 49 +CURRENT_PROJECT_VERSION = 50 SDKROOT = appletvos TARGETED_DEVICE_FAMILY=3 From 3461e1eb9c4086f29f07f9734d6508473c78792d Mon Sep 17 00:00:00 2001 From: RTS Devops Date: Tue, 30 Aug 2022 17:58:44 +0200 Subject: [PATCH 21/35] Bump iOS build number to 395 --- Xcode/Shared/Targets/iOS/Common.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Shared/Targets/iOS/Common.xcconfig b/Xcode/Shared/Targets/iOS/Common.xcconfig index f8086d8c2..033b11b04 100755 --- a/Xcode/Shared/Targets/iOS/Common.xcconfig +++ b/Xcode/Shared/Targets/iOS/Common.xcconfig @@ -2,7 +2,7 @@ // Version information MARKETING_VERSION = 3.6.5 -CURRENT_PROJECT_VERSION = 394 +CURRENT_PROJECT_VERSION = 395 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY=1,2 From 50290ec1c82509269b2fbd8f0b6fe2f44e1db331 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Tue, 30 Aug 2022 22:54:34 +0200 Subject: [PATCH 22/35] Fix what's new --- WhatsNew-iOS-beta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsNew-iOS-beta.json b/WhatsNew-iOS-beta.json index 907ce9797..23e6d2509 100755 --- a/WhatsNew-iOS-beta.json +++ b/WhatsNew-iOS-beta.json @@ -170,5 +170,5 @@ "3.6.3-391": "- The search view now presents topics in addition to the most searched shows.\n- Home page hero stage and highlights should have a better image framing.\n- Improve VoiceOver nagivation by disabling UI large titles.", "3.6.4-392": "- The audio track and subtitle selection window can rotate on iPhone.", "3.6.4-393": "- Fix inverted favorite & subscription banner messages.\n- Remove superfluous pinch gestures on player if device screen ratio fit content image ratio.", - "3.6.5-394": "- Update next recommended media if the user changed the suject in the player view.\n- Add next recommended audio for CarPlay usage.\n- Fix player timeline clickable when playback stopped.\n- Fix player timeline clickable on Mac with Apple silicon." + "3.6.5-394": "- Update next recommended media if the user changed the subject in the player view.\n- Add next recommended audio for CarPlay usage.\n- Fix player timeline clickable when playback stopped.\n- Fix player timeline clickable on Mac with Apple silicon." } \ No newline at end of file From f4ab0470cc05994343c8f005ffec1bd02c8e90d7 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Sat, 3 Sep 2022 12:18:59 +0200 Subject: [PATCH 23/35] Fix documentation --- docs/CUSTOM_URLS_AND_UNIVERSAL_LINKS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CUSTOM_URLS_AND_UNIVERSAL_LINKS.md b/docs/CUSTOM_URLS_AND_UNIVERSAL_LINKS.md index 9d1637dbd..b883c351e 100755 --- a/docs/CUSTOM_URLS_AND_UNIVERSAL_LINKS.md +++ b/docs/CUSTOM_URLS_AND_UNIVERSAL_LINKS.md @@ -12,7 +12,7 @@ For example Play RTS can be opened with URLs starting with `playrts://...`, whil The host name (first item after the `//` in the URL) describes the action which must be performed. Following actions are currently available: -* Open a media within the player: `[scheme]://media/[media_urn]`. An optional `&start_time=[start_time]` parameter can be added to start VOD / AOD playback at the specified position in second. +* Open a media within the player: `[scheme]://media/[media_urn]`. An optional `start_time=[start_time]` parameter can be added to start VOD / AOD playback at the specified position in second. * Open a show page: `[scheme]://show/[show_urn]`. * Open a topic page: `[scheme]://topic/[topic_urn]`. * Open a section page: `[scheme]://section/[section_id]`. From a76d6d32b9a8eb5824e4be3a4927c8ada5c1c23e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Mon, 5 Sep 2022 10:54:21 +0200 Subject: [PATCH 24/35] Fastlane: run betaTester without what's new requirement --- fastlane/Fastfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index db4653246..2cf650176 100755 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -24,8 +24,7 @@ platform :ios do notify_version_to_ci(platform, tag_version, nil) unless lane.to_s.downcase.include?('nightlies') - # For betas and AppStore builds, before all, check if we have a beta release description - if ['appstorebuild', 'beta'].one? { |i| lane.to_s.downcase.include? i } + if what_s_new_condition(lane) what_s_new = what_s_new_for_beta(platform, tag_version) if what_s_new.empty? UI.user_error! "WhatsNew-#{platform}-beta.json has no release notes for #{tag_version}." @@ -1102,6 +1101,12 @@ def app_config CredentialsManager::AppfileConfig end +def what_s_new_condition(lane) + included_lanes = ['appstorebuild', 'beta'] + (!lane.to_s.downcase.include? 'tester') && + included_lanes.one? { |i| lane.to_s.downcase.include? i } +end + def cleaned_lane_condition(lane) excluded_lanes = ['distribute', 'dsym', 'screenshots', 'release', 'tester'] (lane.to_s != 'devLane') && From 5023c84834aa59b604ea3baa03a8af09e4234add Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Wed, 7 Sep 2022 21:44:52 +0200 Subject: [PATCH 25/35] Update unplayable downloaded media (#230) --- Application/Sources/Application/AppDelegate.m | 5 ++ Application/Sources/Model/Download+Private.h | 5 -- Application/Sources/Model/Download.h | 5 ++ Application/Sources/Model/Download.m | 66 ++++++++++++++----- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/Application/Sources/Application/AppDelegate.m b/Application/Sources/Application/AppDelegate.m index 122c1c555..57b547869 100755 --- a/Application/Sources/Application/AppDelegate.m +++ b/Application/Sources/Application/AppDelegate.m @@ -130,6 +130,11 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // Clean downloaded folder [Download removeUnusedDownloadedFiles]; + PlayApplicationRunOnce(^(void (^completionHandler)(BOOL success)) { + [Download updateUnplayableDownloads]; + completionHandler(YES); + }, @"updateUnplayableDownloads"); + [self checkForForcedUpdates]; __block BOOL firstLaunchDone = YES; diff --git a/Application/Sources/Model/Download+Private.h b/Application/Sources/Model/Download+Private.h index eb28c6b3d..d330b1c7f 100755 --- a/Application/Sources/Model/Download+Private.h +++ b/Application/Sources/Model/Download+Private.h @@ -17,11 +17,6 @@ NS_ASSUME_NONNULL_BEGIN */ + (BOOL)addDownload:(Download *)download; -/** - * Clean a folder to ununsed downloaded files - */ -+ (void)removeUnusedDownloadedFilesInFolderPath:(NSString *)folderPath; - /** * Create a download from a dictionary of its fields */ diff --git a/Application/Sources/Model/Download.h b/Application/Sources/Model/Download.h index 8ce598a43..4ce5b106a 100755 --- a/Application/Sources/Model/Download.h +++ b/Application/Sources/Model/Download.h @@ -172,6 +172,11 @@ typedef NS_ENUM(NSInteger, DownloadState) { */ + (void)removeUnusedDownloadedFiles; +/** + * Update the unplayable downloads and their files (or remove if no solutions) + */ ++ (void)updateUnplayableDownloads; + /** * The currently known download progress for a download */ diff --git a/Application/Sources/Model/Download.m b/Application/Sources/Model/Download.m index cc5be1ef0..ef889f505 100755 --- a/Application/Sources/Model/Download.m +++ b/Application/Sources/Model/Download.m @@ -253,7 +253,46 @@ + (NSString *)downloadsDirectoryURLString + (void)removeUnusedDownloadedFiles { - [self removeUnusedDownloadedFilesInFolderPath:[self downloadsDirectoryURLString]]; + NSArray *downloadedMediaFilesURLs = [Download.downloads valueForKey:@keypath(Download.new, localMediaFileURL)]; + NSArray *downloadedImageFilesURLs = [Download.downloads valueForKey:@keypath(Download.new, localImageFileURL)]; + NSArray *allDownloadedFilesURLs = [[@[] arrayByAddingObjectsFromArray:downloadedMediaFilesURLs] arrayByAddingObjectsFromArray:downloadedImageFilesURLs]; + NSString *folderPath = [self downloadsDirectoryURLString]; + NSError *error; + for (NSString *fileName in [NSFileManager.defaultManager contentsOfDirectoryAtPath:folderPath error:&error]) { + NSURL *fileURL = [NSURL fileURLWithPath:[folderPath stringByAppendingPathComponent:fileName]]; + if (! [allDownloadedFilesURLs containsObject:fileURL]) { + [NSFileManager.defaultManager removeItemAtURL:fileURL error:&error]; + } + } +} + ++ (void)updateUnplayableDownloads +{ + NSMutableArray *unplayableDownloadeds = NSMutableArray.array; + for (Download *download in Download.downloads) { + if (download.state == DownloadStateDownloaded && [download.localMediaFileName.pathExtension isEqualToString:@"octet-stream"]) { + // Try to move media file with the download url extension + if (download.downloadMediaURL.pathExtension != nil) { + NSURL *atURL = download.localMediaFileURL; + + NSString *localMediaFileName = [download.localMediaFileName stringByReplacingOccurrencesOfString:download.localMediaFileName.pathExtension + withString:download.downloadMediaURL.pathExtension]; + NSString *mediaFilePath = [[Download downloadsDirectoryURLString] stringByAppendingPathComponent:localMediaFileName]; + NSURL *toURL = [NSURL fileURLWithPath:mediaFilePath]; + [NSFileManager.defaultManager moveItemAtURL:atURL toURL:toURL error:nil]; + + download.localMediaFileName = localMediaFileName; + if (! download.localMediaFileURL) { + [unplayableDownloadeds addObject:download]; + } + } + else { + [unplayableDownloadeds addObject:download]; + } + } + } + [Download removeDownloads:unplayableDownloadeds.copy]; + [self saveDownloadsDictionary]; } #pragma mark Public class methods @@ -365,20 +404,6 @@ + (void)removeAllDownloads [UserInteractionEvent removeFromDownloads:downloads]; } -+ (void)removeUnusedDownloadedFilesInFolderPath:(NSString *)folderPath -{ - NSArray *downloadedMediaFilesURLs = [Download.downloads valueForKey:@keypath(Download.new, localMediaFileURL)]; - NSArray *downloadedImageFilesURLs = [Download.downloads valueForKey:@keypath(Download.new, localImageFileURL)]; - NSArray *allDownloadedFilesURLs = [[@[] arrayByAddingObjectsFromArray:downloadedMediaFilesURLs] arrayByAddingObjectsFromArray:downloadedImageFilesURLs]; - NSError *error; - for (NSString *fileName in [NSFileManager.defaultManager contentsOfDirectoryAtPath:folderPath error:&error]) { - NSURL *fileURL = [NSURL fileURLWithPath:[folderPath stringByAppendingPathComponent:fileName]]; - if (! [allDownloadedFilesURLs containsObject:fileURL]) { - [NSFileManager.defaultManager removeItemAtURL:fileURL error:&error]; - } - } -} - + (nullable NSProgress *)currentlyKnownProgressForDownload:(Download *)download { return [DownloadSession.sharedDownloadSession currentlyKnownProgressForDownload:download]; @@ -512,6 +537,17 @@ - (BOOL)setLocalMediaFileWithTmpFile:(NSURL *)tmpFile MIMEType:(NSString *)MIMET NSString *type = types.firstObject; NSString *extension = types.lastObject ?: @"mov"; + // Try to fix the default arbitrary binary data response with the url file extension + if ([type.lowercaseString isEqualToString:@"application"] && [extension.lowercaseString isEqualToString:@"octet-stream"]) { + if (self.downloadMediaURL.pathExtension != nil) { + extension = self.downloadMediaURL.pathExtension; + } + else { + PlayLogError(@"download", @"Could not find a file extension for media %@.\nMIMEType: %@", self.URN, MIMEType); + return NO; + } + } + // For audio, mpeg type extension don't work with AVPlayer if ([type.lowercaseString isEqualToString:@"audio"] && [extension.lowercaseString isEqualToString:@"mpeg"]) { extension = @"mp3"; From 175e40f91f167227dba70213e182a8f53641092d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Wed, 7 Sep 2022 21:48:00 +0200 Subject: [PATCH 26/35] Update iOS What's new --- WhatsNew-iOS-beta.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WhatsNew-iOS-beta.json b/WhatsNew-iOS-beta.json index 23e6d2509..e0e90c6f9 100755 --- a/WhatsNew-iOS-beta.json +++ b/WhatsNew-iOS-beta.json @@ -170,5 +170,6 @@ "3.6.3-391": "- The search view now presents topics in addition to the most searched shows.\n- Home page hero stage and highlights should have a better image framing.\n- Improve VoiceOver nagivation by disabling UI large titles.", "3.6.4-392": "- The audio track and subtitle selection window can rotate on iPhone.", "3.6.4-393": "- Fix inverted favorite & subscription banner messages.\n- Remove superfluous pinch gestures on player if device screen ratio fit content image ratio.", - "3.6.5-394": "- Update next recommended media if the user changed the subject in the player view.\n- Add next recommended audio for CarPlay usage.\n- Fix player timeline clickable when playback stopped.\n- Fix player timeline clickable on Mac with Apple silicon." + "3.6.5-394": "- Update next recommended media if the user changed the subject in the player view.\n- Add next recommended audio for CarPlay usage.\n- Fix player timeline clickable when playback stopped.\n- Fix player timeline clickable on Mac with Apple silicon.", + "3.6.5-395": "- Update unplayable downloaded media." } \ No newline at end of file From 7b11dff0393e204c7090e6c28202c3d8b5a13f7e Mon Sep 17 00:00:00 2001 From: RTS Devops Date: Wed, 7 Sep 2022 22:54:58 +0200 Subject: [PATCH 27/35] Bump iOS build number to 396 --- Xcode/Shared/Targets/iOS/Common.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Shared/Targets/iOS/Common.xcconfig b/Xcode/Shared/Targets/iOS/Common.xcconfig index 033b11b04..9af0c21ca 100755 --- a/Xcode/Shared/Targets/iOS/Common.xcconfig +++ b/Xcode/Shared/Targets/iOS/Common.xcconfig @@ -2,7 +2,7 @@ // Version information MARKETING_VERSION = 3.6.5 -CURRENT_PROJECT_VERSION = 395 +CURRENT_PROJECT_VERSION = 396 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY=1,2 From 2376db7abca3c7be73a051165c622e9f0787580e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Bertholon Date: Thu, 8 Sep 2022 14:00:56 +0200 Subject: [PATCH 28/35] Update AirShip and swift-collections dependencies --- .../com.mono0926.LicensePlist.latest_result.txt | 4 ++-- .../Settings.bundle/com.mono0926.LicensePlist.plist | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt index 2e6925967..7cb3355ed 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt @@ -135,7 +135,7 @@ name: grpc-ios, nameSpecified: grpc-ios, owner: grpc, version: 1.44.3-grpc, sour name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 2.0.0, source: https://github.com/google/gtm-session-fetcher -name: ios-library, nameSpecified: ios-library, owner: urbanairship, version: 16.9.2, source: https://github.com/urbanairship/ios-library +name: ios-library, nameSpecified: ios-library, owner: urbanairship, version: 16.9.3, source: https://github.com/urbanairship/ios-library name: leveldb, nameSpecified: leveldb, owner: firebase, version: 1.22.2, source: https://github.com/firebase/leveldb @@ -179,7 +179,7 @@ name: srgnetwork-apple, nameSpecified: srgnetwork-apple, owner: SRGSSR, version: name: srguserdata-apple, nameSpecified: srguserdata-apple, owner: SRGSSR, version: 3.3.0, source: https://github.com/SRGSSR/srguserdata-apple -name: swift-collections, nameSpecified: swift-collections, owner: apple, version: 1.0.2, source: https://github.com/apple/swift-collections +name: swift-collections, nameSpecified: swift-collections, owner: apple, version: 1.0.3, source: https://github.com/apple/swift-collections name: swift-protobuf, nameSpecified: swift-protobuf, owner: apple, version: 1.20.1, source: https://github.com/apple/swift-protobuf diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist index 492b75978..3092e82c9 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist @@ -150,7 +150,7 @@ File com.mono0926.LicensePlist/ios-library Title - ios-library (16.9.2) + ios-library (16.9.3) Type PSChildPaneSpecifier @@ -350,7 +350,7 @@ File com.mono0926.LicensePlist/swift-collections Title - swift-collections (1.0.2) + swift-collections (1.0.3) Type PSChildPaneSpecifier diff --git a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved index ad96c0dec..95cbcc19d 100644 --- a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/urbanairship/ios-library.git", "state" : { - "revision" : "be218058500206df98e082126b15728ce3d6b7d9", - "version" : "16.9.2" + "revision" : "12aa2eb9fbdcd3b623346d6771cac02068d6596e", + "version" : "16.9.3" } }, { @@ -321,7 +321,7 @@ "location" : "https://github.com/SRGSSR/srgletterbox-apple.git", "state" : { "branch" : "develop", - "revision" : "1c6545788da0158e26104025463858d629999aee" + "revision" : "188c6eabe4a01145f4aadebea75ba19d2c79cb40" } }, { @@ -365,8 +365,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", - "version" : "1.0.2" + "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", + "version" : "1.0.3" } }, { From e0109b97560001d678c3325a87d5f8bf8f785151 Mon Sep 17 00:00:00 2001 From: Pierre-Yves B Date: Thu, 8 Sep 2022 18:59:48 +0200 Subject: [PATCH 29/35] Add live programs in CarPlay player (#217) It also adds the start over and skip to live buttons. --- .../Play RSI/it.lproj/Localizable.strings | 6 +- .../Play RTR/rm.lproj/Localizable.strings | 4 + .../Play RTS/fr.lproj/Localizable.strings | 4 + .../Play SRF/de.lproj/Localizable.strings | 4 + .../Play SWI/en.lproj/Localizable.strings | 4 + .../skip_to_live.imageset/Contents.json | 23 +++ .../skip_to_live.imageset/skip_to_live-1.pdf | Bin 0 -> 5677 bytes .../skip_to_live.imageset/skip_to_live-2.pdf | Bin 0 -> 5673 bytes .../skip_to_live.imageset/skip_to_live.pdf | Bin 0 -> 5677 bytes .../Images/start_over.imageset/Contents.json | 4 + .../start_over.imageset/start_over-2.pdf | Bin 0 -> 5913 bytes .../Sources/Bridges/PlaySRG-ObjectiveC.h | 1 + .../Sources/CarPlay/CarPlay+Extensions.swift | 4 +- Application/Sources/CarPlay/CarPlayList.swift | 154 +++++++++++++++--- .../CarPlay/CarPlayNowPlayingController.swift | 115 ++++++++++++- .../CarPlay/CarPlaySceneDelegate.swift | 7 - .../Sources/Helpers/AnalyticsConstants.h | 1 + .../Sources/Helpers/AnalyticsConstants.m | 1 + .../SRGLetterboxController+PlaySRG.h | 8 + .../SRGLetterboxController+PlaySRG.m | 10 ++ .../Categories/SRGMediaComposition+PlaySRG.h | 2 +- .../Categories/SRGMediaComposition+PlaySRG.m | 2 +- .../Sources/MiniPlayer/PlayMiniPlayerView.m | 7 +- .../Player/MediaPlayerViewController.m | 26 +-- .../Sources/UI/Helpers/MediaDescription.swift | 2 +- PlaySRG.xcodeproj/project.pbxproj | 2 +- 26 files changed, 323 insertions(+), 68 deletions(-) create mode 100644 Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/Contents.json create mode 100644 Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live-1.pdf create mode 100644 Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live-2.pdf create mode 100644 Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live.pdf create mode 100644 Application/Resources/Images/CommonImages.xcassets/Images/start_over.imageset/start_over-2.pdf diff --git a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings index 279e0524c..374ec3c15 100755 --- a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings @@ -377,7 +377,7 @@ "Most listened to" = "I piΓΉ ascoltati"; /* Most searched shows header */ -"Most searched shows" = "Programmi piΓΉ ricercati"; +"Most searched shows" = "Programmi piΓΉ cercati"; /* Song list title */ "Music" = "Musica"; @@ -488,6 +488,10 @@ /* Title of the button to proceed to the previous onboarding page */ "Previous" = "Precedente"; +/* Button title on CarPlay player for livestream previous programs + Livestream previous programs screen title */ +"Previous shows" = "Trasmissioni precedenti"; + /* Server setting name */ "Production" = "Produzione"; diff --git a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings index 96f116ad5..3cbf0a95d 100755 --- a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings @@ -488,6 +488,10 @@ /* Title of the button to proceed to the previous onboarding page */ "Previous" = "Precedent"; +/* Button title on CarPlay player for livestream previous programs + Livestream previous programs screen title */ +"Previous shows" = "Emissiuns precedentas"; + /* Server setting name */ "Production" = "Producziun"; diff --git a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings index e9e34ca11..0511054ba 100644 --- a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings @@ -488,6 +488,10 @@ /* Title of the button to proceed to the previous onboarding page */ "Previous" = "PrΓ©cΓ©dent"; +/* Button title on CarPlay player for livestream previous programs + Livestream previous programs screen title */ +"Previous shows" = "Γ‰missions prΓ©cΓ©dentes"; + /* Server setting name */ "Production" = "Production"; diff --git a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings index 777c033ed..d9baf0d0c 100755 --- a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings @@ -488,6 +488,10 @@ /* Title of the button to proceed to the previous onboarding page */ "Previous" = "ZurΓΌck"; +/* Button title on CarPlay player for livestream previous programs + Livestream previous programs screen title */ +"Previous shows" = "Vorherige Sendungen"; + /* Server setting name */ "Production" = "Production"; diff --git a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings index 6acbcc493..cc2d2e603 100755 --- a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings @@ -488,6 +488,10 @@ /* Title of the button to proceed to the previous onboarding page */ "Previous" = "Previous"; +/* Button title on CarPlay player for livestream previous programs + Livestream previous programs screen title */ +"Previous shows" = "Previous shows"; + /* Server setting name */ "Production" = "Production"; diff --git a/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/Contents.json b/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/Contents.json new file mode 100644 index 000000000..1ba8fa1e5 --- /dev/null +++ b/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "skip_to_live.pdf", + "idiom" : "iphone" + }, + { + "filename" : "skip_to_live-1.pdf", + "idiom" : "ipad" + }, + { + "filename" : "skip_to_live-2.pdf", + "idiom" : "car" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live-1.pdf b/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live-1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7c0735bede4fc3fdfc60d708d66e1e871786a31a GIT binary patch literal 5677 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 Jt(Xkd{{g7ph1viB literal 0 HcmV?d00001 diff --git a/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live.pdf b/Application/Resources/Images/CommonImages.xcassets/Images/skip_to_live.imageset/skip_to_live.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7c0735bede4fc3fdfc60d708d66e1e871786a31a GIT binary patch literal 5677 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 N(c-GAL`x&_{{x=hA)^2Q literal 0 HcmV?d00001 diff --git a/Application/Sources/Bridges/PlaySRG-ObjectiveC.h b/Application/Sources/Bridges/PlaySRG-ObjectiveC.h index 6aa331849..34d12be39 100755 --- a/Application/Sources/Bridges/PlaySRG-ObjectiveC.h +++ b/Application/Sources/Bridges/PlaySRG-ObjectiveC.h @@ -43,6 +43,7 @@ #import "SRGDay+PlaySRG.h" #import "SRGLetterboxController+PlaySRG.h" #import "SRGMedia+PlaySRG.h" +#import "SRGMediaComposition+PlaySRG.h" #import "SRGProgram+PlaySRG.h" #import "SRGProgramComposition+PlaySRG.h" #import "UIColor+PlaySRG.h" diff --git a/Application/Sources/CarPlay/CarPlay+Extensions.swift b/Application/Sources/CarPlay/CarPlay+Extensions.swift index 031397cc9..5e3528b80 100644 --- a/Application/Sources/CarPlay/CarPlay+Extensions.swift +++ b/Application/Sources/CarPlay/CarPlay+Extensions.swift @@ -23,7 +23,9 @@ extension CPListTemplate { extension CPInterfaceController { func play(media: SRGMedia, completion: @escaping () -> Void) { if let controller = SRGLetterboxService.shared.controller { - controller.playMedia(media, at: HistoryResumePlaybackPositionForMedia(media), withPreferredSettings: ApplicationSettingPlaybackSettings()) + if controller.play_mainMedia != media { + controller.playMedia(media, at: HistoryResumePlaybackPositionForMedia(media), withPreferredSettings: ApplicationSettingPlaybackSettings()) + } } else { let controller = SRGLetterboxController() diff --git a/Application/Sources/CarPlay/CarPlayList.swift b/Application/Sources/CarPlay/CarPlayList.swift index 65e34b3fe..590c798d0 100644 --- a/Application/Sources/CarPlay/CarPlayList.swift +++ b/Application/Sources/CarPlay/CarPlayList.swift @@ -15,6 +15,7 @@ enum CarPlayList { case livestreams case mostPopular case mostPopularMedias(radioChannel: RadioChannel) + case livePrograms(channel: SRGChannel, media: SRGMedia) private static let pageSize: UInt = 20 @@ -28,6 +29,8 @@ enum CarPlayList { return NSLocalizedString("Trends", comment: "Tab title to present the most popular medias by channel on CarPlay") case let .mostPopularMedias(radioChannel: radioChannel): return radioChannel.name + case .livePrograms: + return "\(NSLocalizedString("Previous shows", comment: "Livestream previous programs screen title"))" } } @@ -39,6 +42,8 @@ enum CarPlayList { return AnalyticsPageTitle.home.rawValue case .mostPopularMedias: return AnalyticsPageTitle.mostPopular.rawValue + case .livePrograms: + return AnalyticsPageTitle.livePrograms.rawValue } } @@ -52,6 +57,8 @@ enum CarPlayList { return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, AnalyticsPageLevel.mostPopular.rawValue] case let .mostPopularMedias(radioChannel): return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, radioChannel.name] + case let .livePrograms(channel, _): + return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, channel.title] } } @@ -73,6 +80,10 @@ enum CarPlayList { case let .mostPopularMedias(radioChannel: radioChannel): return SRGDataProvider.current!.radioMostPopularMedias(for: ApplicationConfiguration.shared.vendor, channelUid: radioChannel.uid, pageSize: Self.pageSize) .mapToSections(with: interfaceController) + case let .livePrograms(channel, media): + return Publishers.PublishAndRepeat(onOutputFrom: Timer.publish(every: 30, on: .main, in: .common).autoconnect()) { + return Self.liveProgramsSections(for: channel, media: media, interfaceController: interfaceController) + } } } } @@ -83,6 +94,12 @@ private extension CarPlayList { let playing: Bool } + struct LiveProgramData { + let program: SRGProgram + let image: UIImage + let playing: Bool + } + struct MediaData { let media: SRGMedia let image: UIImage @@ -91,17 +108,28 @@ private extension CarPlayList { } static func liveMediaDataPublisher(for media: SRGMedia) -> AnyPublisher { - return playingPublisher(for: media) + return playingPublisher(for: media.urn) .map { playing in return LiveMediaData(media: media, playing: playing) } .eraseToAnyPublisher() } + static func liveProgramDataPublisher(for program: SRGProgram) -> AnyPublisher { + return Publishers.CombineLatest( + imagePublisher(for: program), + playingPublisher(for: program.mediaURN) + ) + .map { image, playing in + return LiveProgramData(program: program, image: image, playing: playing) + } + .eraseToAnyPublisher() + } + static func mediaDataPublisher(for media: SRGMedia) -> AnyPublisher { return Publishers.CombineLatest3( imagePublisher(for: media), - playingPublisher(for: media), + playingPublisher(for: media.urn), UserDataPublishers.playbackProgressPublisher(for: media) ) .map { image, playing, progress in @@ -110,16 +138,30 @@ private extension CarPlayList { .eraseToAnyPublisher() } - private static func playingPublisher(for media: SRGMedia) -> AnyPublisher { - return nowPlayingMediaPublisher() - .map { media == $0 } - .eraseToAnyPublisher() + private static func playingPublisher(for mediaUrn: String?) -> AnyPublisher { + if let mediaUrn = mediaUrn { + return nowPlayingMediaPublisher() + .map { $0.map(\.urn).contains(mediaUrn) } + .eraseToAnyPublisher() + } + else { + return Just(false) + .eraseToAnyPublisher() + } } private static func imagePublisher(for media: SRGMedia) -> AnyPublisher { + return imagePublisher(for: media.image) + } + + private static func imagePublisher(for program: SRGProgram) -> AnyPublisher { + return imagePublisher(for: program.image) + } + + private static func imagePublisher(for image: SRGImage?) -> AnyPublisher { let imageSize = SRGImageSize.small let placeholderImage = UIColor.placeholder.image(ofSize: SRGRecommendedImageCGSize(imageSize, .default)) - if let imageUrl = url(for: media.image, size: imageSize) { + if let imageUrl = url(for: image, size: imageSize) { return ImagePipeline.shared.imagePublisher(with: imageUrl) .map(\.image) .replaceError(with: placeholderImage) @@ -132,21 +174,24 @@ private extension CarPlayList { } } - private static func nowPlayingMedia(for controller: SRGLetterboxController?) -> SRGMedia? { - guard let controller = controller else { return nil } - if let fullLengthMedia = controller.fullLengthMedia, fullLengthMedia.contentType == .livestream || fullLengthMedia.contentType == .scheduledLivestream { - return fullLengthMedia + private static func nowPlayingMedia(for controller: SRGLetterboxController?) -> [SRGMedia] { + guard let controller = controller else { return [] } + + var medias: Set = [] + if let mainMedia = controller.play_mainMedia { + medias.insert(mainMedia) } - else { - return controller.media + if let media = controller.media { + medias.insert(media) } + return Array(medias) } - private static func nowPlayingMediaPublisher() -> AnyPublisher { + private static func nowPlayingMediaPublisher() -> AnyPublisher<[SRGMedia], Never> { return SRGLetterboxService.shared.publisher(for: \.controller) - .map { controller -> AnyPublisher in + .map { controller -> AnyPublisher<[SRGMedia], Never> in if let controller = controller { - return NotificationCenter.default.weakPublisher(for: NSNotification.Name.SRGLetterboxMetadataDidChange, object: controller) + return NotificationCenter.default.weakPublisher(for: .SRGLetterboxMetadataDidChange, object: controller) .map { notification in let controller = notification.object as? SRGLetterboxController return nowPlayingMedia(for: controller) @@ -155,7 +200,7 @@ private extension CarPlayList { .eraseToAnyPublisher() } else { - return Just(nil) + return Just([]) .eraseToAnyPublisher() } } @@ -163,6 +208,29 @@ private extension CarPlayList { .removeDuplicates() .eraseToAnyPublisher() } + + private static func liveProgramsPublisher(for channel: SRGChannel, media: SRGMedia) -> AnyPublisher<[SRGProgram], Error> { + if let controller = SRGLetterboxService.shared.controller, + let dateInterval = controller.play_dateInterval, + let segments = controller.mediaComposition?.mainChapter.segments, !segments.isEmpty { + return SRGDataProvider.current!.radioLatestPrograms(for: ApplicationConfiguration.shared.vendor, + channelUid: channel.uid, + livestreamUid: media.uid, + from: nil, to: nil, + pageSize: 50, paginatedBy: nil) + .map { _, programs in + return programs + .filter { $0.startDate >= dateInterval.start && $0.startDate <= dateInterval.end } + .reversed() + } + .eraseToAnyPublisher() + } + else { + return Just([]) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + } } // MARK: Protocols @@ -197,14 +265,15 @@ private extension CarPlayList { }) } .switchToLatest() - .map { mediaDataList in - let items = mediaDataList.map { mediaData -> CPListItem in - let item = CPListItem(text: mediaData.media.channel?.title, detailText: nil, image: Self.logoImage(for: mediaData.media)) + .map { liveMediaDataList in + let items = liveMediaDataList.map { liveMediaData -> CPListItem in + let item = CPListItem(text: liveMediaData.media.channel?.title, detailText: nil, image: Self.logoImage(for: liveMediaData.media)) item.accessoryType = .none item.handler = { _, completion in - interfaceController.play(media: mediaData.media, completion: completion) + interfaceController.play(media: liveMediaData.media, completion: completion) } - item.isPlaying = mediaData.playing + item.playingIndicatorLocation = .trailing + item.isPlaying = liveMediaData.playing return item } return [CPListSection(items: items)] @@ -237,6 +306,39 @@ private extension CarPlayList { .eraseToAnyPublisher() } } + + static func liveProgramsSections(for channel: SRGChannel, media: SRGMedia, interfaceController: CPInterfaceController) -> AnyPublisher<[CPListSection], Error> { + return liveProgramsPublisher(for: channel, media: media) + .map { programs in + return Publishers.AccumulateLatestMany(programs.map { program in + return liveProgramDataPublisher(for: program) + }) + } + .switchToLatest() + .map { liveProgramDataList in + let items = liveProgramDataList.map { liveProgramData -> CPListItem in + let program = liveProgramData.program + let time = "\(DateFormatter.play_time.string(from: program.startDate)) - \(DateFormatter.play_time.string(from: program.endDate))" + let item = CPListItem(text: liveProgramData.program.title, detailText: time, image: liveProgramData.image) + item.accessoryType = .none + item.handler = { _, completion in + if let mediaUrn = program.mediaURN, program.startDate <= Date() { + SRGLetterboxService.shared.controller?.switch(toURN: mediaUrn, withCompletionHandler: { _ in + completion() + }) + } + else { + completion() + } + } + item.playingIndicatorLocation = .trailing + item.isPlaying = liveProgramData.playing + return item + } + return [CPListSection(items: items)] + } + .eraseToAnyPublisher() + } } private extension Publisher where Output == [SRGMedia] { @@ -250,14 +352,16 @@ private extension Publisher where Output == [SRGMedia] { .map { mediaDataList in let items = mediaDataList.map { mediaData -> CPListItem in let item = CPListItem(text: MediaDescription.title(for: mediaData.media, style: .show), - detailText: MediaDescription.subtitle(for: mediaData.media, style: .show), + // Keep same media item height with a detail text in any cases. + detailText: MediaDescription.subtitle(for: mediaData.media, style: .show) ?? " ", image: mediaData.image) - item.isPlaying = mediaData.playing - item.playbackProgress = mediaData.progress ?? 0 item.accessoryType = .none item.handler = { _, completion in interfaceController.play(media: mediaData.media, completion: completion) } + item.playingIndicatorLocation = .trailing + item.isPlaying = mediaData.playing + item.playbackProgress = mediaData.progress ?? 0 return item } return [CPListSection(items: items)] diff --git a/Application/Sources/CarPlay/CarPlayNowPlayingController.swift b/Application/Sources/CarPlay/CarPlayNowPlayingController.swift index febc2dbab..b24446ad2 100644 --- a/Application/Sources/CarPlay/CarPlayNowPlayingController.swift +++ b/Application/Sources/CarPlay/CarPlayNowPlayingController.swift @@ -10,9 +10,10 @@ import SRGLetterbox // MARK: Controller -final class CarPlayNowPlayingController { +final class CarPlayNowPlayingController: NSObject { private weak var interfaceController: CPInterfaceController? - private var cancellables = Set() + private var popToRootCancellable: AnyCancellable + private var nowPlayingPropertiesCancellable: AnyCancellable? init(interfaceController: CPInterfaceController) { self.interfaceController = interfaceController @@ -20,19 +21,107 @@ final class CarPlayNowPlayingController { // If the player is closed on the iOS device return to the first level. A better result would inspect the // template hierarchy to pop to the previous one but this might perform an IPC call. Popping to the root // should be sufficient. - SRGLetterboxService.shared.publisher(for: \.controller) + popToRootCancellable = SRGLetterboxService.shared.publisher(for: \.controller) .filter { $0 == nil } .sink { [weak interfaceController] _ in interfaceController?.popToRootTemplate(animated: true) { _, _ in } } - .store(in: &cancellables) + + CPNowPlayingTemplate.shared.upNextTitle = NSLocalizedString("Previous shows", comment: "Button title on CarPlay player for livestream previous programs") + } +} + +private extension CarPlayNowPlayingController { + private struct NowPlayingProperties: Equatable { + let nowPlayingButtons: [CPNowPlayingButton] + let upNextButtonEnabled: Bool + + init(for controller: SRGLetterboxController?, interfaceController: CPInterfaceController) { + nowPlayingButtons = Self.nowPlayingButtons(for: controller, interfaceController: interfaceController) + upNextButtonEnabled = Self.upNextButtonEnabled(for: controller) + } + + private static func playbackRateButton(for interfaceController: CPInterfaceController) -> CPNowPlayingButton { + return CPNowPlayingImageButton(image: UIImage(systemName: "speedometer")!) { _ in + interfaceController.pushTemplate(CPListTemplate.playbackRate, animated: true) { _, _ in } + } + } + + private static func startOverButton() -> CPNowPlayingButton { + return CPNowPlayingImageButton(image: UIImage(named: "start_over", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in + SRGLetterboxService.shared.controller?.startOver() + } + } + + private static func skipToLiveButton() -> CPNowPlayingButton { + return CPNowPlayingImageButton(image: UIImage(named: "skip_to_live", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in + SRGLetterboxService.shared.controller?.skipToLive() + } + } + + private static func nowPlayingButtons(for controller: SRGLetterboxController?, interfaceController: CPInterfaceController) -> [CPNowPlayingButton] { + guard let controller = controller else { return [] } + + var nowPlayingButtons = [playbackRateButton(for: interfaceController)] + if controller.canStartOver() { + nowPlayingButtons.insert(startOverButton(), at: 0) + } + if controller.canSkipToLive() { + nowPlayingButtons.append(skipToLiveButton()) + } + return nowPlayingButtons + } + + private static func upNextButtonEnabled(for controller: SRGLetterboxController?) -> Bool { + if let mainChapter = controller?.mediaComposition?.mainChapter, mainChapter.contentType == .livestream, + let segments = mainChapter.segments { + return !segments.isEmpty + } + else { + return false + } + } + } + + private static func nowPlayingPropertiesPublisher(interfaceController: CPInterfaceController) -> AnyPublisher { + return SRGLetterboxService.shared.publisher(for: \.controller) + .map { controller -> AnyPublisher in + if let controller = controller { + return Publishers.CombineLatest3( + controller.mediaPlayerController.publisher(for: \.timeRange), + NotificationCenter.default.weakPublisher(for: .SRGLetterboxPlaybackStateDidChange, object: controller), + NotificationCenter.default.weakPublisher(for: .SRGLetterboxMetadataDidChange, object: controller) + ) + .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) + .map { _ in + return NowPlayingProperties(for: controller, interfaceController: interfaceController) + } + .prepend(NowPlayingProperties(for: controller, interfaceController: interfaceController)) + .eraseToAnyPublisher() + } + else { + return Just(NowPlayingProperties(for: controller, interfaceController: interfaceController)) + .eraseToAnyPublisher() + } + } + .switchToLatest() + .removeDuplicates() + .eraseToAnyPublisher() } } // MARK: Protocols extension CarPlayNowPlayingController: CarPlayTemplateController { - func willAppear(animated: Bool) {} + func willAppear(animated: Bool) { + CPNowPlayingTemplate.shared.add(self) + nowPlayingPropertiesCancellable = Self.nowPlayingPropertiesPublisher(interfaceController: interfaceController!) + .sink { nowPlayingProperties in + let template = CPNowPlayingTemplate.shared + template.updateNowPlayingButtons(nowPlayingProperties.nowPlayingButtons) + template.isUpNextButtonEnabled = nowPlayingProperties.upNextButtonEnabled + } + } func didAppear(animated: Bool) { SRGAnalyticsTracker.shared.uncheckedTrackPageView( @@ -43,5 +132,19 @@ extension CarPlayNowPlayingController: CarPlayTemplateController { func willDisappear(animated: Bool) {} - func didDisappear(animated: Bool) {} + func didDisappear(animated: Bool) { + nowPlayingPropertiesCancellable = nil + CPNowPlayingTemplate.shared.remove(self) + } +} + +extension CarPlayNowPlayingController: CPNowPlayingTemplateObserver { + func nowPlayingTemplateUpNextButtonTapped(_ nowPlayingTemplate: CPNowPlayingTemplate) { + if let channel = SRGLetterboxService.shared.controller?.channel, + let media = SRGLetterboxService.shared.controller?.play_mainMedia, + let interfaceController = interfaceController { + let template = CPListTemplate.list(.livePrograms(channel: channel, media: media), interfaceController: interfaceController) + interfaceController.pushTemplate(template, animated: true) { _, _ in } + } + } } diff --git a/Application/Sources/CarPlay/CarPlaySceneDelegate.swift b/Application/Sources/CarPlay/CarPlaySceneDelegate.swift index 91eaccf17..9b2cd3d9c 100644 --- a/Application/Sources/CarPlay/CarPlaySceneDelegate.swift +++ b/Application/Sources/CarPlay/CarPlaySceneDelegate.swift @@ -15,16 +15,9 @@ final class CarPlaySceneDelegate: UIResponder { // MARK: Protocols extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate { - private static func playbackRateButton(for interfaceController: CPInterfaceController) -> CPNowPlayingButton { - return CPNowPlayingImageButton(image: UIImage(systemName: "speedometer")!) { _ in - interfaceController.pushTemplate(CPListTemplate.playbackRate, animated: true) { _, _ in } - } - } - private static func configureNowPlayingTemplate(for interfaceController: CPInterfaceController) { let nowPlayingTemplate = CPNowPlayingTemplate.shared nowPlayingTemplate.controller = CarPlayNowPlayingController(interfaceController: interfaceController) - nowPlayingTemplate.updateNowPlayingButtons([playbackRateButton(for: interfaceController)]) } func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) { diff --git a/Application/Sources/Helpers/AnalyticsConstants.h b/Application/Sources/Helpers/AnalyticsConstants.h index 4c73937b3..d08f4d4a7 100755 --- a/Application/Sources/Helpers/AnalyticsConstants.h +++ b/Application/Sources/Helpers/AnalyticsConstants.h @@ -47,6 +47,7 @@ OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleLatestEpisodes; OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleLatestEpisodesFromFavorites; OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleLicense; OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleLicenses; +OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleLivePrograms; OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleLogin; OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleMedia; OBJC_EXPORT AnalyticsPageTitle const AnalyticsPageTitleMostPopular; diff --git a/Application/Sources/Helpers/AnalyticsConstants.m b/Application/Sources/Helpers/AnalyticsConstants.m index fc6acfb8a..ec24f27e4 100755 --- a/Application/Sources/Helpers/AnalyticsConstants.m +++ b/Application/Sources/Helpers/AnalyticsConstants.m @@ -37,6 +37,7 @@ AnalyticsPageTitle const AnalyticsPageTitleLatestEpisodesFromFavorites = @"latest episodes from favorites"; AnalyticsPageTitle const AnalyticsPageTitleLicense = @"license"; AnalyticsPageTitle const AnalyticsPageTitleLicenses = @"licenses"; +AnalyticsPageTitle const AnalyticsPageTitleLivePrograms = @"live programs"; AnalyticsPageTitle const AnalyticsPageTitleLogin = @"login"; AnalyticsPageTitle const AnalyticsPageTitleMedia = @"media"; AnalyticsPageTitle const AnalyticsPageTitleMostPopular = @"most popular"; diff --git a/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.h b/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.h index e8b3eec2c..691537684 100644 --- a/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.h +++ b/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.h @@ -12,6 +12,14 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) NSDateInterval *play_dateInterval; +@property (nonatomic, readonly, nullable) SRGMedia *play_mainMedia; + +@end + +@interface SRGLetterboxController (PlaySRG_Private) + +@property (nonatomic, readonly) SRGMediaPlayerController *mediaPlayerController; + @end NS_ASSUME_NONNULL_END diff --git a/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.m b/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.m index 0ac08578d..8c87c6e3f 100644 --- a/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.m +++ b/Application/Sources/Helpers/Categories/SRGLetterboxController+PlaySRG.m @@ -21,4 +21,14 @@ - (NSDateInterval *)play_dateInterval } } +- (SRGMedia *)play_mainMedia +{ + if (self.mediaComposition) { + return [self.mediaComposition mediaForSubdivision:self.mediaComposition.mainChapter]; + } + else { + return self.media; + } +} + @end diff --git a/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.h b/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.h index a8b147ee7..4cac10b4a 100755 --- a/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.h +++ b/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN * Check chapters and segments from the receiver and return the first subdivision matching the specified URN. If no * match is found, `nil` is returned. */ -- (nullable SRGSubdivision *)subdivisionWithURN:(NSString *)URN; +- (nullable SRGSubdivision *)play_subdivisionWithURN:(NSString *)URN; @end diff --git a/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.m b/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.m index 82ce3c97d..6ab14b957 100755 --- a/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.m +++ b/Application/Sources/Helpers/Categories/SRGMediaComposition+PlaySRG.m @@ -10,7 +10,7 @@ @implementation SRGMediaComposition (PlaySRG) -- (SRGSubdivision *)subdivisionWithURN:(NSString *)URN +- (SRGSubdivision *)play_subdivisionWithURN:(NSString *)URN { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @keypath(SRGSubdivision.new, URN), URN]; SRGSubdivision *subdivision = [self.chapters filteredArrayUsingPredicate:predicate].firstObject; diff --git a/Application/Sources/MiniPlayer/PlayMiniPlayerView.m b/Application/Sources/MiniPlayer/PlayMiniPlayerView.m index 59af7e34c..638b08dfc 100755 --- a/Application/Sources/MiniPlayer/PlayMiniPlayerView.m +++ b/Application/Sources/MiniPlayer/PlayMiniPlayerView.m @@ -15,6 +15,7 @@ #import "History.h" #import "MediaPlayerViewController.h" #import "NSBundle+PlaySRG.h" +#import "SRGLetterboxController+PlaySRG.h" #import "SRGProgram+PlaySRG.h" #import "SRGProgramComposition+PlaySRG.h" #import "UIView+PlaySRG.h" @@ -45,12 +46,6 @@ @interface PlayMiniPlayerView ()