feat: report authoritative playback progress from shared backend
This commit is contained in:
+92
-14
@@ -5,7 +5,8 @@ import TerebiConfiguration
|
||||
@MainActor
|
||||
final class PlayerModel: ObservableObject {
|
||||
static var startupConfiguration = PlayerConfiguration()
|
||||
static let shared = PlayerModel(configuration: startupConfiguration)
|
||||
static var startupProgressFile: String?
|
||||
static let shared = PlayerModel(configuration: startupConfiguration, progressFile: startupProgressFile)
|
||||
|
||||
@Published private(set) var state = PlayerState()
|
||||
@Published private(set) var fileName: String?
|
||||
@@ -80,8 +81,16 @@ final class PlayerModel: ObservableObject {
|
||||
private var resumePlaying = true
|
||||
private var lastSavedPosition: Double?
|
||||
private var systemPlayback: SystemPlaybackControls?
|
||||
private(set) var progressInitializationError: String?
|
||||
private var reportingEnabled = false
|
||||
private var reportingErrorShown = false
|
||||
private var reportingErrorBuffer = [CChar]()
|
||||
private var reportingErrorMessage: String {
|
||||
reportingErrorBuffer.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }
|
||||
}
|
||||
|
||||
init(defaults: UserDefaults = .standard, configuration: PlayerConfiguration = PlayerConfiguration(),
|
||||
progressFile: String? = nil,
|
||||
qualityClock: @escaping () -> Double = { ProcessInfo.processInfo.systemUptime }) {
|
||||
self.qualityClock = qualityClock
|
||||
keyBindings = configuration.keys
|
||||
@@ -99,6 +108,15 @@ final class PlayerModel: ObservableObject {
|
||||
videoView = nil
|
||||
commandError = "Could not create the media player."
|
||||
}
|
||||
if let progressFile {
|
||||
reportingErrorBuffer = [CChar](repeating: 0, count: 512)
|
||||
if let player {
|
||||
reportingEnabled = progressFile.withCString { player_set_progress_file(player, $0, &reportingErrorBuffer) }
|
||||
if !reportingEnabled { progressInitializationError = reportingErrorMessage }
|
||||
} else {
|
||||
progressInitializationError = "Could not create the media player to initialize progress reporting."
|
||||
}
|
||||
}
|
||||
compactWindow.onChange = { [weak self] in
|
||||
guard let self else { return }
|
||||
if !self.compactWindow.isTransitioning {
|
||||
@@ -288,20 +306,25 @@ final class PlayerModel: ObservableObject {
|
||||
guard let handle else { return }
|
||||
saveProgress()
|
||||
let revision = player_get_state(handle).revision
|
||||
let accepted = video.video.url.withCString { videoURL in
|
||||
video.video.headers.withCString { videoHeaders in
|
||||
func enqueue(audioURL: UnsafePointer<CChar>?, audioHeaders: UnsafePointer<CChar>?) -> Bool {
|
||||
var source = PlayerNetworkSource(video_url: videoURL, audio_url: audioURL,
|
||||
video_headers: videoHeaders, audio_headers: audioHeaders,
|
||||
duration: video.duration, is_live: video.isLive)
|
||||
return player_open_network(handle, &source)
|
||||
}
|
||||
if let audio = video.audio {
|
||||
return audio.url.withCString { audioURL in
|
||||
audio.headers.withCString { enqueue(audioURL: audioURL, audioHeaders: $0) }
|
||||
let initialPosition = video.isLive ? 0 :
|
||||
(restoring.map { $0.position ?? 0 } ?? playbackProgress.position(for: video.webpageURL) ?? 0)
|
||||
let accepted = video.webpageURL.absoluteString.withCString { mediaURL in
|
||||
video.video.url.withCString { videoURL in
|
||||
video.video.headers.withCString { videoHeaders in
|
||||
func enqueue(audioURL: UnsafePointer<CChar>?, audioHeaders: UnsafePointer<CChar>?) -> Bool {
|
||||
var source = PlayerNetworkSource(video_url: videoURL, audio_url: audioURL,
|
||||
video_headers: videoHeaders, audio_headers: audioHeaders,
|
||||
duration: video.duration, is_live: video.isLive,
|
||||
media_url: mediaURL, initial_position: initialPosition)
|
||||
return player_open_network(handle, &source)
|
||||
}
|
||||
if let audio = video.audio {
|
||||
return audio.url.withCString { audioURL in
|
||||
audio.headers.withCString { enqueue(audioURL: audioURL, audioHeaders: $0) }
|
||||
}
|
||||
}
|
||||
return enqueue(audioURL: nil, audioHeaders: nil)
|
||||
}
|
||||
return enqueue(audioURL: nil, audioHeaders: nil)
|
||||
}
|
||||
}
|
||||
guard accepted else {
|
||||
@@ -338,7 +361,11 @@ final class PlayerModel: ObservableObject {
|
||||
youtubeHeight = nil
|
||||
youtubeQuality = nil
|
||||
progressURL = url
|
||||
if let restoring {
|
||||
if !url.isFileURL {
|
||||
// Network restoration is part of the backend open transaction, so
|
||||
// reporting cannot observe a pre-resume position as initial state.
|
||||
resumePosition = nil
|
||||
} else if let restoring {
|
||||
resumePosition = restoring.position
|
||||
} else {
|
||||
resumePosition = live ? nil : playbackProgress.position(for: url)
|
||||
@@ -729,6 +756,11 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
private func poll() {
|
||||
guard let handle else { return }
|
||||
if reportingEnabled && !reportingErrorShown {
|
||||
if player_get_progress_error(handle, &reportingErrorBuffer) {
|
||||
showReportingError(reportingErrorMessage)
|
||||
}
|
||||
}
|
||||
applyState(player_get_state(handle))
|
||||
}
|
||||
|
||||
@@ -798,6 +830,21 @@ final class PlayerModel: ObservableObject {
|
||||
resolveYouTube(url, height: height, automatic: true)
|
||||
}
|
||||
|
||||
private func showReportingError(_ message: String, terminating: Bool = false) {
|
||||
guard !reportingErrorShown else { return }
|
||||
reportingErrorShown = true
|
||||
FileHandle.standardError.write(Data("terebi: \(message)\n".utf8))
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = "Playback progress reporting failed"
|
||||
alert.informativeText = message + "\n\nReporting has been disabled. Playback is unaffected."
|
||||
if let window, !terminating {
|
||||
alert.beginSheetModal(for: window)
|
||||
} else {
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
saveProgress()
|
||||
cancelYouTubeResolution()
|
||||
@@ -809,6 +856,11 @@ final class PlayerModel: ObservableObject {
|
||||
hideDeadline = nil
|
||||
NSCursor.setHiddenUntilMouseMoves(false)
|
||||
guard let player = handle else { return }
|
||||
if reportingEnabled {
|
||||
if !player_shutdown(player, &reportingErrorBuffer) {
|
||||
showReportingError(reportingErrorMessage, terminating: true)
|
||||
}
|
||||
}
|
||||
handle = nil
|
||||
player_destroy(player)
|
||||
}
|
||||
@@ -1526,5 +1578,31 @@ public func terebiAppMain() {
|
||||
alert.runModal()
|
||||
exit(78)
|
||||
}
|
||||
let arguments = Array(CommandLine.arguments.dropFirst())
|
||||
if let index = arguments.firstIndex(of: "--progress-file") {
|
||||
guard index + 1 < arguments.count, arguments[index + 1].hasPrefix("/"),
|
||||
arguments.filter({ $0 == "--progress-file" }).count == 1 else {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "Could not initialize playback progress reporting"
|
||||
alert.informativeText = "--progress-file requires one absolute filesystem path."
|
||||
alert.runModal()
|
||||
exit(73)
|
||||
}
|
||||
PlayerModel.startupProgressFile = arguments[index + 1]
|
||||
_ = NSApplication.shared
|
||||
if let error = PlayerModel.shared.progressInitializationError {
|
||||
PlayerModel.shared.shutdown()
|
||||
FileHandle.standardError.write(Data("terebi: \(error)\n".utf8))
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "Could not initialize playback progress reporting"
|
||||
alert.informativeText = error
|
||||
alert.runModal()
|
||||
exit(73)
|
||||
}
|
||||
}
|
||||
TerebiApp.main()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user