From 04fc467cb4f01b66936468df5e6a3b6aa6e955fd Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:33:34 +0200 Subject: [PATCH] not 100% sure byt maybe it works now --- .../NormalPlayer/NormalPlayer.swift | 22 ++ .../MediaUtils/NormalPlayer/VideoPlayer.swift | 228 ++++++------------ .../SharePlay/SharePlayCoordinator.swift | 78 ------ .../SharePlay/SharePlayManager.swift | 77 ------ .../SharePlay/VideoWatchingActivity.swift | 35 +-- Sulfur.xcodeproj/project.pbxproj | 8 - 6 files changed, 106 insertions(+), 342 deletions(-) delete mode 100644 Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift delete mode 100644 Sora/MediaUtils/SharePlay/SharePlayManager.swift diff --git a/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift b/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift index da9e9c3..ed4f53d 100644 --- a/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift @@ -6,15 +6,37 @@ // import AVKit +import GroupActivities class NormalPlayer: AVPlayerViewController { private var originalRate: Float = 1.0 private var holdGesture: UILongPressGestureRecognizer? + var onSharePlayRequested: (() -> Void)? + override func viewDidLoad() { super.viewDidLoad() setupHoldGesture() setupAudioSession() + setupSharePlayButton() + } + + private func setupSharePlayButton() { + let sharePlayItem = UIBarButtonItem( + image: UIImage(systemName: "shareplay"), + style: .plain, + target: self, + action: #selector(sharePlayButtonTapped) + ) + sharePlayItem.tintColor = .white + + if responds(to: Selector(("setCustomControlItems:"))) { + setValue([sharePlayItem], forKey: "customControlItems") + } + } + + @objc private func sharePlayButtonTapped() { + onSharePlayRequested?() } private func setupHoldGesture() { diff --git a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index c5f3e27..e963f03 100644 --- a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -28,12 +28,9 @@ class VideoPlayerViewController: UIViewController { var episodeNumber: Int = 0 var episodeImageUrl: String = "" var mediaTitle: String = "" - var subtitlesLoader: VTTSubtitlesLoader? - var subtitleLabel: UILabel? - private var sharePlayCoordinator: SharePlayCoordinator? + private var groupSession: GroupSession? private var subscriptions = Set() - private var groupSessionObserver: AnyCancellable? private var aniListUpdateSent = false private var aniListUpdatedSuccessfully = false @@ -46,60 +43,12 @@ class VideoPlayerViewController: UIViewController { if UserDefaults.standard.object(forKey: "subtitlesEnabled") == nil { UserDefaults.standard.set(true, forKey: "subtitlesEnabled") } - setupSharePlay() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupSubtitles() { - guard !subtitles.isEmpty, UserDefaults.standard.bool(forKey: "subtitlesEnabled"), let _ = URL(string: subtitles) else { - return - } - - subtitlesLoader = VTTSubtitlesLoader() - setupSubtitleLabel() - - subtitlesLoader?.load(from: subtitles) - - let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in - self?.updateSubtitles(at: time.seconds) - } - } - - private func setupSubtitleLabel() { - let label = UILabel() - label.numberOfLines = 0 - label.textAlignment = .center - label.textColor = .white - label.font = .systemFont(ofSize: 16, weight: .medium) - label.layer.shadowColor = UIColor.black.cgColor - label.layer.shadowOffset = CGSize(width: 1, height: 1) - label.layer.shadowOpacity = 0.8 - label.layer.shadowRadius = 2 - - guard let playerView = playerViewController?.view else { return } - playerView.addSubview(label) - label.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: playerView.leadingAnchor, constant: 16), - label.trailingAnchor.constraint(equalTo: playerView.trailingAnchor, constant: -16), - label.bottomAnchor.constraint(equalTo: playerView.bottomAnchor, constant: -32) - ]) - - self.subtitleLabel = label - } - - private func updateSubtitles(at time: Double) { - let currentSubtitle = subtitlesLoader?.cues.first { cue in - time >= cue.startTime && time <= cue.endTime - } - subtitleLabel?.text = currentSubtitle?.text ?? "" - } - override func viewDidLoad() { super.viewDidLoad() @@ -133,13 +82,11 @@ class VideoPlayerViewController: UIViewController { view.addSubview(playerViewController.view) playerViewController.didMove(toParent: self) - if !subtitles.isEmpty && UserDefaults.standard.bool(forKey: "subtitlesEnabled") { - setupSubtitles() + playerViewController.onSharePlayRequested = { [weak self] in + Task { @MainActor in + await self?.startSharePlay() + } } - - // Configure SharePlay after player setup - setupSharePlayButton(in: playerViewController) - configureSharePlayForPlayer() } addPeriodicTimeObserver(fullURL: fullUrl) @@ -153,27 +100,86 @@ class VideoPlayerViewController: UIViewController { self.player?.play() } - observeGroupSession() + configureGroupSession() } - private func observeGroupSession() { - groupSessionObserver = nil - Task { [weak self] in - guard let self = self else { return } - for await session in VideoWatchingActivity.sessions() { - await self.handleIncomingGroupSession(session) + private func configureGroupSession() { + Task { + for await groupSession in VideoWatchingActivity.sessions() { + await configureGroupSession(groupSession) } } } @MainActor - private func handleIncomingGroupSession(_ session: GroupSession) async { - if sharePlayCoordinator == nil { - sharePlayCoordinator = SharePlayCoordinator() + private func configureGroupSession(_ groupSession: GroupSession) async { + self.groupSession = groupSession + + groupSession.$state + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + switch state { + case .joined: + self?.coordinatePlayback() + case .invalidated: + self?.groupSession = nil + default: + break + } + } + .store(in: &subscriptions) + + groupSession.join() + } + + private func coordinatePlayback() { + guard let player = player, let groupSession = groupSession else { return } + + player.playbackCoordinator.coordinateWithSession(groupSession) + } + + @MainActor + func startSharePlay() async { + guard let streamUrl = streamUrl else { return } + + var episodeImageData: Data? + if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { + do { + episodeImageData = try await URLSession.shared.data(from: imageUrl).0 + } catch { + Logger.shared.log("Failed to load episode image: \(error)", type: "Error") + } } - sharePlayCoordinator?.configureGroupSession() - if let player = self.player { - sharePlayCoordinator?.coordinatePlayback(with: player) + + let activity = VideoWatchingActivity( + mediaTitle: mediaTitle, + episodeNumber: episodeNumber, + streamUrl: streamUrl, + subtitles: subtitles, + aniListID: aniListID, + fullUrl: fullUrl, + headers: headers, + episodeImageUrl: episodeImageUrl, + episodeImageData: episodeImageData, + totalEpisodes: totalEpisodes, + tmdbID: tmdbID, + isMovie: isMovie, + seasonNumber: seasonNumber + ) + + do { + _ = try await activity.activate() + Logger.shared.log("SharePlay session started successfully", type: "SharePlay") + } catch { + Logger.shared.log("Failed to start SharePlay: \(error)", type: "Error") + + let alert = UIAlertController( + title: "SharePlay Unavailable", + message: "SharePlay is not available right now. Make sure you're connected to FaceTime or have SharePlay enabled in Control Center.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) } } @@ -309,79 +315,6 @@ class VideoPlayerViewController: UIViewController { } } - @MainActor - private func setupSharePlay() { - sharePlayCoordinator = SharePlayCoordinator() - sharePlayCoordinator?.configureGroupSession() - - if let playerViewController = playerViewController { - setupSharePlayButton(in: playerViewController) - } - } - - private func setupSharePlayButton(in playerViewController: NormalPlayer) { - // WIP - } - - @MainActor - private func startSharePlay() { - guard let streamUrl = streamUrl else { return } - - Task { - var episodeImageData: Data? - if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { - episodeImageData = try? await URLSession.shared.data(from: imageUrl).0 - } - - let activity = VideoWatchingActivity( - mediaTitle: mediaTitle, - episodeNumber: episodeNumber, - streamUrl: streamUrl, - subtitles: subtitles, - aniListID: aniListID, - fullUrl: fullUrl, - headers: headers, - episodeImageUrl: episodeImageUrl, - episodeImageData: episodeImageData, - totalEpisodes: totalEpisodes, - tmdbID: tmdbID, - isMovie: isMovie, - seasonNumber: seasonNumber - ) - - await sharePlayCoordinator?.startSharePlay(with: activity) - } - } - - private func configureSharePlayForPlayer() { - guard let player = player else { return } - sharePlayCoordinator?.coordinatePlayback(with: player) - } - - @MainActor - func presentSharePlayInvitation() { - guard let streamUrl = streamUrl else { - Logger.shared.log("Cannot start SharePlay: Stream URL is nil", type: "Error") - return - } - - SharePlayManager.shared.presentSharePlayInvitation( - from: self, - mediaTitle: mediaTitle, - episodeNumber: episodeNumber, - streamUrl: streamUrl, - subtitles: subtitles, - aniListID: aniListID, - fullUrl: fullUrl, - headers: headers, - episodeImageUrl: episodeImageUrl, - totalEpisodes: totalEpisodes, - tmdbID: tmdbID, - isMovie: isMovie, - seasonNumber: seasonNumber - ) - } - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape @@ -403,13 +336,8 @@ class VideoPlayerViewController: UIViewController { if let timeObserverToken = timeObserverToken { player?.removeTimeObserver(timeObserverToken) } - subtitleLabel?.removeFromSuperview() - subtitleLabel = nil - subtitlesLoader = nil - sharePlayCoordinator?.leaveGroupSession() - sharePlayCoordinator = nil + groupSession?.leave() subscriptions.removeAll() - groupSessionObserver = nil } } diff --git a/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift b/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift deleted file mode 100644 index dea9d61..0000000 --- a/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// SharePlayCoordinator.swift -// Sora -// -// Created by Francesco on 15/06/25. -// - -import Combine -import Foundation -import AVFoundation -import GroupActivities - -@MainActor -class SharePlayCoordinator: ObservableObject { - private var subscriptions = Set() - private var groupSession: GroupSession? - - @Published var isEligibleForGroupSession = false - @Published var groupSessionState: GroupSession.State = .waiting - - private var playbackCoordinator: AVPlayerPlaybackCoordinator? - - func configureGroupSession() { - Task { - for await session in VideoWatchingActivity.sessions() { - await configureGroupSession(session) - } - } - } - - private func configureGroupSession(_ groupSession: GroupSession) async { - self.groupSession = groupSession - - groupSession.$state - .receive(on: DispatchQueue.main) - .assign(to: &$groupSessionState) - - groupSession.$activeParticipants - .receive(on: DispatchQueue.main) - .sink { participants in - Logger.shared.log("Active participants: \(participants.count)", type: "SharePlay") - } - .store(in: &subscriptions) - - groupSession.join() - } - - func startSharePlay(with activity: VideoWatchingActivity) async { - do { - _ = try await activity.activate() - Logger.shared.log("SharePlay activity activated successfully", type: "SharePlay") - } catch { - Logger.shared.log("Failed to activate SharePlay: \(error.localizedDescription)", type: "Error") - } - } - - func coordinatePlayback(with player: AVPlayer) { - guard let groupSession = groupSession else { return } - - playbackCoordinator = player.playbackCoordinator - playbackCoordinator?.coordinateWithSession(groupSession) - - Logger.shared.log("Playback coordination established", type: "SharePlay") - } - - nonisolated func leaveGroupSession() { - Task { @MainActor in - self.groupSession?.leave() - self.playbackCoordinator = nil - Logger.shared.log("Left SharePlay session", type: "SharePlay") - } - } - - deinit { - subscriptions.removeAll() - playbackCoordinator = nil - } -} diff --git a/Sora/MediaUtils/SharePlay/SharePlayManager.swift b/Sora/MediaUtils/SharePlay/SharePlayManager.swift deleted file mode 100644 index 415510c..0000000 --- a/Sora/MediaUtils/SharePlay/SharePlayManager.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// SharePlayManager.swift -// Sora -// -// Created by Francesco on 15/06/25. -// - -import UIKit -import Foundation -import GroupActivities - -class SharePlayManager { - static let shared = SharePlayManager() - - private init() {} - - func isSharePlayAvailable() -> Bool { - return true - } - - func presentSharePlayInvitation(from viewController: UIViewController, - mediaTitle: String, - episodeNumber: Int, - streamUrl: String, - subtitles: String = "", - aniListID: Int = 0, - fullUrl: String, - headers: [String: String]? = nil, - episodeImageUrl: String = "", - totalEpisodes: Int = 0, - tmdbID: Int? = nil, - isMovie: Bool = false, - seasonNumber: Int = 1) { - - Task { @MainActor in - var episodeImageData: Data? - if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { - do { - episodeImageData = try await URLSession.shared.data(from: imageUrl).0 - } catch { - Logger.shared.log("Failed to load episode image for SharePlay: \(error.localizedDescription)", type: "Error") - } - } - - let activity = VideoWatchingActivity( - mediaTitle: mediaTitle, - episodeNumber: episodeNumber, - streamUrl: streamUrl, - subtitles: subtitles, - aniListID: aniListID, - fullUrl: fullUrl, - headers: headers, - episodeImageUrl: episodeImageUrl, - episodeImageData: episodeImageData, - totalEpisodes: totalEpisodes, - tmdbID: tmdbID, - isMovie: isMovie, - seasonNumber: seasonNumber - ) - - do { - _ = try await activity.activate() - Logger.shared.log("SharePlay invitation sent successfully", type: "SharePlay") - } catch { - Logger.shared.log("Failed to send SharePlay invitation: \(error.localizedDescription)", type: "Error") - - let alert = UIAlertController( - title: "SharePlay Unavailable", - message: "SharePlay is not available right now. Make sure you're connected to FaceTime or have SharePlay enabled in Control Center.", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - viewController.present(alert, animated: true) - } - } - } -} diff --git a/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift b/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift index f055604..df64851 100644 --- a/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift +++ b/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift @@ -13,7 +13,12 @@ struct VideoWatchingActivity: GroupActivity { var metadata: GroupActivityMetadata { var metadata = GroupActivityMetadata() metadata.title = mediaTitle - metadata.subtitle = "Episode \(episodeNumber)" + + if isMovie { + metadata.subtitle = "Movie" + } else { + metadata.subtitle = "Episode \(episodeNumber)" + } if let imageData = episodeImageData, let uiImage = UIImage(data: imageData) { @@ -37,32 +42,4 @@ struct VideoWatchingActivity: GroupActivity { let tmdbID: Int? let isMovie: Bool let seasonNumber: Int - - init(mediaTitle: String, - episodeNumber: Int, - streamUrl: String, - subtitles: String = "", - aniListID: Int = 0, - fullUrl: String, - headers: [String: String]? = nil, - episodeImageUrl: String = "", - episodeImageData: Data? = nil, - totalEpisodes: Int = 0, - tmdbID: Int? = nil, - isMovie: Bool = false, - seasonNumber: Int = 1) { - self.mediaTitle = mediaTitle - self.episodeNumber = episodeNumber - self.streamUrl = streamUrl - self.subtitles = subtitles - self.aniListID = aniListID - self.fullUrl = fullUrl - self.headers = headers - self.episodeImageUrl = episodeImageUrl - self.episodeImageData = episodeImageData - self.totalEpisodes = totalEpisodes - self.tmdbID = tmdbID - self.isMovie = isMovie - self.seasonNumber = seasonNumber - } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 4b9be87..c3488b9 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -45,8 +45,6 @@ 13367ECC2DF70698009CB33F /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECB2DF70698009CB33F /* Nuke */; }; 13367ECE2DF70698009CB33F /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECD2DF70698009CB33F /* NukeUI */; }; 133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */; }; - 133CF6A72DFEBE9000BD13F9 /* SharePlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */; }; - 133CF6A82DFEBE9000BD13F9 /* SharePlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; @@ -149,8 +147,6 @@ 132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = ""; }; 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWatchingActivity.swift; sourceTree = ""; }; - 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayManager.swift; sourceTree = ""; }; - 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayCoordinator.swift; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -398,8 +394,6 @@ 133CF6A22DFEBE8100BD13F9 /* SharePlay */ = { isa = PBXGroup; children = ( - 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */, - 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */, 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */, ); path = SharePlay; @@ -899,7 +893,6 @@ 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 0457C5A12DE78385000AFBD9 /* BookmarksDetailView.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, - 133CF6A82DFEBE9000BD13F9 /* SharePlayCoordinator.swift in Sources */, 13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, 13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */, @@ -927,7 +920,6 @@ 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */, 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */, - 133CF6A72DFEBE9000BD13F9 /* SharePlayManager.swift in Sources */, 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */, 0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */, );