ok well lets test

This commit is contained in:
cranci1 2025-06-15 10:50:35 +02:00
parent 634bda6a94
commit 0ca54ea38d
15 changed files with 351 additions and 9 deletions

View file

@ -7,6 +7,8 @@
import UIKit
import AVKit
import Combine
import GroupActivities
class VideoPlayerViewController: UIViewController {
let module: ScrapingModule
@ -29,6 +31,9 @@ class VideoPlayerViewController: UIViewController {
var subtitlesLoader: VTTSubtitlesLoader?
var subtitleLabel: UILabel?
private var sharePlayCoordinator: SharePlayCoordinator?
private var subscriptions = Set<AnyCancellable>()
private var aniListUpdateSent = false
private var aniListUpdatedSuccessfully = false
private var traktUpdateSent = false
@ -40,6 +45,7 @@ class VideoPlayerViewController: UIViewController {
if UserDefaults.standard.object(forKey: "subtitlesEnabled") == nil {
UserDefaults.standard.set(true, forKey: "subtitlesEnabled")
}
setupSharePlay()
}
required init?(coder: NSCoder) {
@ -129,6 +135,10 @@ class VideoPlayerViewController: UIViewController {
if !subtitles.isEmpty && UserDefaults.standard.bool(forKey: "subtitlesEnabled") {
setupSubtitles()
}
// Configure SharePlay after player setup
setupSharePlayButton(in: playerViewController)
configureSharePlayForPlayer()
}
addPeriodicTimeObserver(fullURL: fullUrl)
@ -275,6 +285,79 @@ 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
@ -299,5 +382,9 @@ class VideoPlayerViewController: UIViewController {
subtitleLabel?.removeFromSuperview()
subtitleLabel = nil
subtitlesLoader = nil
sharePlayCoordinator?.leaveGroupSession()
sharePlayCoordinator = nil
subscriptions.removeAll()
}
}

View file

@ -0,0 +1,78 @@
//
// 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<AnyCancellable>()
private var groupSession: GroupSession<VideoWatchingActivity>?
@Published var isEligibleForGroupSession = false
@Published var groupSessionState: GroupSession<VideoWatchingActivity>.State = .waiting
private var playbackCoordinator: AVPlayerPlaybackCoordinator?
func configureGroupSession() {
Task {
for await session in VideoWatchingActivity.sessions() {
await configureGroupSession(session)
}
}
}
private func configureGroupSession(_ groupSession: GroupSession<VideoWatchingActivity>) 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
}
}

View file

@ -0,0 +1,77 @@
//
// 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)
}
}
}
}

View file

@ -0,0 +1,68 @@
//
// VideoWatchingActivity.swift
// Sora
//
// Created by Francesco on 15/06/25.
//
import UIKit
import Foundation
import GroupActivities
struct VideoWatchingActivity: GroupActivity {
var metadata: GroupActivityMetadata {
var metadata = GroupActivityMetadata()
metadata.title = mediaTitle
metadata.subtitle = "Episode \(episodeNumber)"
if let imageData = episodeImageData,
let uiImage = UIImage(data: imageData) {
metadata.previewImage = uiImage.cgImage
}
metadata.type = .watchTogether
return metadata
}
let mediaTitle: String
let episodeNumber: Int
let streamUrl: String
let subtitles: String
let aniListID: Int
let fullUrl: String
let headers: [String: String]?
let episodeImageUrl: String
let episodeImageData: Data?
let totalEpisodes: Int
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
}
}

View file

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.group-session</key>
<true/>
<key>com.apple.developer.group-session.video</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.assets.movies.read-write</key>

View file

@ -40,6 +40,9 @@
132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; };
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 */; };
@ -137,6 +140,9 @@
132AF1202D99951700A0140B /* JSController-Streams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Streams.swift"; sourceTree = "<group>"; };
132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = "<group>"; };
132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = "<group>"; };
133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWatchingActivity.swift; sourceTree = "<group>"; };
133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayManager.swift; sourceTree = "<group>"; };
133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayCoordinator.swift; sourceTree = "<group>"; };
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 = "<group>"; };
133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -349,6 +355,27 @@
path = Analytics;
sourceTree = "<group>";
};
133CF6A22DFEBE8100BD13F9 /* SharePlay */ = {
isa = PBXGroup;
children = (
133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */,
133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */,
133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */,
);
path = SharePlay;
sourceTree = "<group>";
};
133CF6A92DFEBEAB00BD13F9 /* MediaUtils */ = {
isa = PBXGroup;
children = (
133CF6A22DFEBE8100BD13F9 /* SharePlay */,
13DC0C442D302C6A00D0F966 /* NormalPlayer */,
13EA2BD02D32D97400C1EBD7 /* CustomPlayer */,
13C0E5E82D5F85DD00E7F619 /* ContinueWatching */,
);
path = MediaUtils;
sourceTree = "<group>";
};
133D7C612D2BE2500075467E = {
isa = PBXGroup;
children = (
@ -368,15 +395,16 @@
133D7C6C2D2BE2500075467E /* Sora */ = {
isa = PBXGroup;
children = (
0488FA9B2DFDF385007575E1 /* ar.lproj */,
0488FA972DFDF334007575E1 /* fr.lproj */,
0488FA912DFDE724007575E1 /* en.lproj */,
0488FA942DFDE724007575E1 /* nl.lproj */,
0488FA912DFDE724007575E1 /* en.lproj */,
0488FA972DFDF334007575E1 /* fr.lproj */,
0488FA9B2DFDF385007575E1 /* ar.lproj */,
130C6BF82D53A4C200DC1432 /* Sora.entitlements */,
13DC0C412D2EC9BA00D0F966 /* Info.plist */,
13103E802D589D6C000F0673 /* Tracking Services */,
133D7C852D2BE2640075467E /* Utils */,
133CF6A92DFEBEAB00BD13F9 /* MediaUtils */,
133D7C7B2D2BE2630075467E /* Views */,
133D7C852D2BE2640075467E /* Utils */,
133D7C6D2D2BE2500075467E /* SoraApp.swift */,
133D7C6F2D2BE2500075467E /* ContentView.swift */,
133D7C712D2BE2520075467E /* Assets.xcassets */,
@ -446,10 +474,8 @@
133D7C8A2D2BE2640075467E /* JSLoader */,
1327FBA52D758CEA00FC6689 /* Analytics */,
133D7C862D2BE2640075467E /* Extensions */,
13DC0C442D302C6A00D0F966 /* MediaPlayer */,
13103E8C2D58E037000F0673 /* SkeletonCells */,
72443C832DC8046500A61321 /* DownloadUtils */,
13C0E5E82D5F85DD00E7F619 /* ContinueWatching */,
);
path = Utils;
sourceTree = "<group>";
@ -593,14 +619,13 @@
path = Auth;
sourceTree = "<group>";
};
13DC0C442D302C6A00D0F966 /* MediaPlayer */ = {
13DC0C442D302C6A00D0F966 /* NormalPlayer */ = {
isa = PBXGroup;
children = (
13EA2BD02D32D97400C1EBD7 /* CustomPlayer */,
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */,
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */,
);
path = MediaPlayer;
path = NormalPlayer;
sourceTree = "<group>";
};
13E62FBF2DABC3A20007E259 /* Trakt */ = {
@ -806,6 +831,7 @@
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
722248662DCBC13E00CABE2D /* JSController-Downloads.swift in Sources */,
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */,
13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */,
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */,
722248632DCBAA4700CABE2D /* JSController-HeaderManager.swift in Sources */,
@ -813,6 +839,7 @@
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 */,
@ -840,6 +867,7 @@
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 */,
);