Merge branch 'dev'

This commit is contained in:
cranci1 2025-06-16 10:40:18 +02:00
commit f0f41c378a
6 changed files with 106 additions and 342 deletions

View file

@ -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() {

View file

@ -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<VideoWatchingActivity>?
private var subscriptions = Set<AnyCancellable>()
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<VideoWatchingActivity>) async {
if sharePlayCoordinator == nil {
sharePlayCoordinator = SharePlayCoordinator()
private func configureGroupSession(_ groupSession: GroupSession<VideoWatchingActivity>) 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
}
}

View file

@ -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<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

@ -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)
}
}
}
}

View file

@ -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
}
}

View file

@ -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 = "<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>"; };
@ -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 */,
);