132 lines
6.5 KiB
Swift
132 lines
6.5 KiB
Swift
//
|
|
// DamusVideoCoordinator.swift
|
|
// damus
|
|
//
|
|
// Created by Bryan Montz on 9/3/23.
|
|
//
|
|
|
|
import Combine
|
|
import Foundation
|
|
import SwiftUI
|
|
import AVFoundation
|
|
|
|
/// DamusVideoCoordinator is responsible for coordinating the various video players throughout the app, and providing a nicely orchestrated experience.
|
|
/// The goals of this object are to:
|
|
/// - ensure some video playing states (such as mute state and current time) are consistent across different video player view instances of the same video
|
|
/// - ensure only one video is playing at a time
|
|
/// - Provide global video playback controls to control the currently playing video
|
|
///
|
|
/// This is used as a singleton object (one global object per `DamusState`), which gets passed around to video players, which can then interact with the coordinator to ensure an app-wide coherent experience
|
|
///
|
|
/// A good analogy here is that video players and their models/states are like individual cars and their drivers, and this coordinator is like a traffic control person + traffic lights that ensures cars don't crash each other.
|
|
final class DamusVideoCoordinator: ObservableObject {
|
|
// MARK: - States
|
|
|
|
// MARK: State and information about each video
|
|
private var players: [URL: DamusVideoPlayer] = [:]
|
|
|
|
// MARK: Main stage requests from player views
|
|
// The stacks of video player views that have marked themselves as visible on the user screen.
|
|
//
|
|
// Because our visibility tracker cannot tell if a player is obscured by a view in front of it,
|
|
// we need to implement two stacks representing the different view layers:
|
|
// - Normal layer: For timelines, threads, etc
|
|
// - Full screen layer: For full screen views
|
|
|
|
private var normal_layer_main_stage_requests: [MainStageRequest] = []
|
|
private var full_screen_layer_stage_requests: [MainStageRequest] = []
|
|
|
|
// MARK: Coordinator state
|
|
// Members representing the state of the coordinator itself
|
|
|
|
private var full_screen_mode: Bool = false {
|
|
didSet {
|
|
self.select_focused_video()
|
|
}
|
|
}
|
|
|
|
/// The video currently in focus
|
|
/// This can only be chosen by the coordinator. To get a video in focus, use one of the instance methods that provide an interface for focus control.
|
|
@MainActor
|
|
@Published private(set) var focused_video: DamusVideoPlayer? {
|
|
didSet {
|
|
oldValue?.pause()
|
|
focused_video?.play()
|
|
Log.info("VIDEO_COORDINATOR: %s paused, playing %s", for: .video_coordination, oldValue?.url.absoluteString ?? "no video", focused_video?.url.absoluteString ?? "no video")
|
|
}
|
|
}
|
|
|
|
// MARK: - Interface to set and fetch information about each different video
|
|
|
|
|
|
@MainActor
|
|
func get_player(for url: URL) -> DamusVideoPlayer {
|
|
if let player = self.players[url] {
|
|
return player
|
|
}
|
|
let player = DamusVideoPlayer(url: url)
|
|
self.players[url] = player
|
|
return player
|
|
}
|
|
|
|
|
|
// MARK: - Interface for video players to come to the foreground
|
|
// This portion provides an interface for video players to signal their visibility changes,
|
|
// and implements some coordination logic to choose which video to play and pause at a given time.
|
|
|
|
func request_main_stage(_ request: MainStageRequest) {
|
|
Log.info("VIDEO_COORDINATOR: %s requested main stage", for: .video_coordination, request.requestor_id.uuidString)
|
|
switch request.layer_context {
|
|
case .normal_layer:
|
|
if normal_layer_main_stage_requests.first(where: { $0.requestor_id == request.requestor_id }) != nil { return } // Entry exists already
|
|
normal_layer_main_stage_requests.append(request)
|
|
case .full_screen_layer:
|
|
if full_screen_layer_stage_requests.first(where: { $0.requestor_id == request.requestor_id }) != nil { return } // Entry exists already
|
|
full_screen_layer_stage_requests.append(request)
|
|
}
|
|
self.select_focused_video()
|
|
}
|
|
|
|
func give_up_main_stage(request_id: UUID) {
|
|
Log.info("VIDEO_COORDINATOR: %s gave up the main stage", for: .video_coordination, request_id.uuidString)
|
|
normal_layer_main_stage_requests.removeAll(where: { $0.requestor_id == request_id })
|
|
full_screen_layer_stage_requests.removeAll(where: { $0.requestor_id == request_id })
|
|
self.select_focused_video()
|
|
}
|
|
|
|
// MARK: - Additional interface to help with video coordination
|
|
|
|
func set_full_screen_mode(_ is_full_screen: Bool) {
|
|
full_screen_mode = is_full_screen
|
|
}
|
|
|
|
// MARK: - Internal video coordination logic
|
|
|
|
private func select_focused_video() {
|
|
// This function may be called during a SwiftUI view update,
|
|
// so schedule this change for the next render pass to ensure state immutability/stability within a single render pass
|
|
DispatchQueue.main.async { [weak self] in // [weak self] to safeguard in cases this object is deallocated by the time we execute this task
|
|
guard let self else { return }
|
|
// The focused video will always be the last one that was inserted — similar to a LIFO stack
|
|
// The reason is that:
|
|
// - both a LIFO stack and a FIFO queue are decent at selecting videos when scrolling on the Y axis (timeline),
|
|
// - The LIFO stack is better at selecting videos when navigating on the Z axis (e.g. opening and closing full screen covers or sheets), since those sheets operate like a stack as well
|
|
let winning_request = self.full_screen_mode ? self.full_screen_layer_stage_requests.last : self.normal_layer_main_stage_requests.last
|
|
self.focused_video = winning_request?.player
|
|
winning_request?.main_stage_granted?()
|
|
}
|
|
Log.info("VIDEO_COORDINATOR: fullscreen layer main stage request stack: %s", for: .video_coordination, full_screen_layer_stage_requests.map({ $0.requestor_id.uuidString }).debugDescription)
|
|
Log.info("VIDEO_COORDINATOR: normal layer main stage request stack: %s", for: .video_coordination, normal_layer_main_stage_requests.map({ $0.requestor_id.uuidString }).debugDescription)
|
|
Log.info("VIDEO_COORDINATOR: full_screen_mode: %s", for: .video_coordination, String(describing: self.full_screen_mode))
|
|
}
|
|
|
|
// MARK: - Helper structures
|
|
|
|
struct MainStageRequest {
|
|
var requestor_id: UUID
|
|
var layer_context: ViewLayerContext
|
|
var player: DamusVideoPlayer
|
|
var main_stage_granted: (() -> Void)?
|
|
}
|
|
}
|