feat: enqueue streams in running player
This commit is contained in:
+85
-21
@@ -50,6 +50,7 @@ final class PlayerModel: ObservableObject {
|
||||
private var queuePlaybackRevision: UInt64?
|
||||
private var queueCompletionHandled = false
|
||||
private var queueCastDevice: CastDevice?
|
||||
private var resolvedQueueSource: (id: UUID, source: ResolvedSource)?
|
||||
var canPreviousQueueItem: Bool { canOpen && queue.canPrevious }
|
||||
var canNextQueueItem: Bool { canOpen && queue.canNext }
|
||||
private var transition = PlaybackTransition()
|
||||
@@ -218,24 +219,8 @@ final class PlayerModel: ObservableObject {
|
||||
progressInitializationError = "Could not create the media player to initialize progress reporting."
|
||||
}
|
||||
}
|
||||
if let streamQueue {
|
||||
reportingErrorBuffer = [CChar](repeating: 0, count: 512)
|
||||
for entry in streamQueue.entries {
|
||||
guard let player else {
|
||||
progressInitializationError = "Could not create the media player to initialize queue reports."
|
||||
break
|
||||
}
|
||||
let registered = entry.identity.withCString { identity in
|
||||
entry.progressFile.path.withCString { path in
|
||||
player_register_progress_file(player, identity, path, &reportingErrorBuffer)
|
||||
}
|
||||
}
|
||||
if !registered {
|
||||
progressInitializationError = reportingErrorMessage
|
||||
break
|
||||
}
|
||||
reportingEnabled = true
|
||||
}
|
||||
if let streamQueue, !registerQueueReports(streamQueue) {
|
||||
progressInitializationError = reportingErrorMessage
|
||||
}
|
||||
compactWindow.onChange = { [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -380,6 +365,12 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
showsYouTubeInput = false
|
||||
clearQueue()
|
||||
let url: URL
|
||||
switch stream {
|
||||
case .youtube(let canonical): url = canonical
|
||||
case .miruro: url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines))!
|
||||
}
|
||||
queue.replaceStreams([(url: url, label: url.absoluteString)])
|
||||
resolveStream(stream, change: .open)
|
||||
}
|
||||
|
||||
@@ -393,6 +384,69 @@ final class PlayerModel: ObservableObject {
|
||||
openQueueSelection(keepingReceiver: false)
|
||||
}
|
||||
|
||||
private func registerQueueReports(_ streamQueue: StreamQueue) -> Bool {
|
||||
reportingErrorBuffer = [CChar](repeating: 0, count: 512)
|
||||
guard let handle else { return false }
|
||||
let identities = streamQueue.entries.map { strdup($0.identity) }
|
||||
let paths = streamQueue.entries.map { strdup($0.progressFile.path) }
|
||||
defer {
|
||||
for value in identities { free(value) }
|
||||
for value in paths { free(value) }
|
||||
}
|
||||
let identityPointers = identities.map { $0.map { UnsafePointer($0) } }
|
||||
let pathPointers = paths.map { $0.map { UnsafePointer($0) } }
|
||||
let accepted = player_register_progress_files(handle, identityPointers, pathPointers,
|
||||
Int32(streamQueue.entries.count), &reportingErrorBuffer)
|
||||
if accepted { reportingEnabled = true }
|
||||
return accepted
|
||||
}
|
||||
|
||||
func enqueueStreamQueue(_ streamQueue: StreamQueue) {
|
||||
guard canOpen else {
|
||||
commandError = "Could not create the media player to enqueue streams."
|
||||
return
|
||||
}
|
||||
let existing = Set(queue.items.compactMap { item -> String? in
|
||||
guard !item.url.isFileURL else { return nil }
|
||||
if let stream = try? StreamURL.parse(item.url.absoluteString) {
|
||||
switch stream {
|
||||
case .youtube(let url): return url.absoluteString
|
||||
case .miruro(let id, let episode): return "miruro:\(id):\(episode)"
|
||||
}
|
||||
}
|
||||
return item.url.absoluteString
|
||||
})
|
||||
guard !streamQueue.entries.contains(where: { existing.contains($0.identity) }) else {
|
||||
commandError = "Cannot enqueue a video already present in this queue; reports cannot be rebound."
|
||||
revealControls()
|
||||
return
|
||||
}
|
||||
guard registerQueueReports(streamQueue) else {
|
||||
commandError = reportingErrorMessage
|
||||
revealControls()
|
||||
return
|
||||
}
|
||||
let needsStart = queue.currentID == nil && currentMedia == nil && !resolvingStream
|
||||
queue.appendStreams(streamQueue.entries.map { (url: $0.url, label: $0.label) })
|
||||
commandError = nil
|
||||
showsQueue = queue.items.count > 1
|
||||
if needsStart, let first = queue.items.first {
|
||||
queue.select(first.id)
|
||||
if fileName == nil { configureStartWindow(showing: false) }
|
||||
openQueueSelection(keepingReceiver: false)
|
||||
}
|
||||
}
|
||||
|
||||
func receiveEnqueueURL(_ url: URL) {
|
||||
do {
|
||||
let file = try StreamQueue.enqueueFile(from: url)
|
||||
enqueueStreamQueue(try StreamQueue.load(from: file))
|
||||
} catch {
|
||||
commandError = error.localizedDescription
|
||||
revealControls()
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveStream(_ stream: StreamURL, change: MediaChange) {
|
||||
replaceMetadata(for: nil)
|
||||
switch stream {
|
||||
@@ -405,6 +459,8 @@ final class PlayerModel: ObservableObject {
|
||||
do {
|
||||
let source = try ResolvedSource.load(from: file)
|
||||
clearQueue()
|
||||
queue.replaceStreams([(url: URL(string: source.identity)!, label: source.title)])
|
||||
resolvedQueueSource = (queue.currentID!, source)
|
||||
cancelStreamResolution()
|
||||
openMedia(.resolved(source))
|
||||
} catch {
|
||||
@@ -761,7 +817,9 @@ final class PlayerModel: ObservableObject {
|
||||
queueCompletionHandled = true
|
||||
cancelStreamResolution()
|
||||
let change = MediaChange.queue(playing: receiver == nil)
|
||||
if url.isFileURL {
|
||||
if let original = resolvedQueueSource, queue.currentID == original.id {
|
||||
openMedia(.resolved(original.source), change: change)
|
||||
} else if url.isFileURL {
|
||||
openMedia(.file(url), change: change)
|
||||
} else {
|
||||
do { resolveStream(try StreamURL.parse(url.absoluteString), change: change) }
|
||||
@@ -773,6 +831,7 @@ final class PlayerModel: ObservableObject {
|
||||
private func clearQueue() {
|
||||
queue.replace([])
|
||||
queuePlaybackRevision = nil
|
||||
resolvedQueueSource = nil
|
||||
queueCompletionHandled = false
|
||||
queueCastDevice = nil
|
||||
}
|
||||
@@ -3542,7 +3601,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
@objc private func openInternetURL(_ event: NSAppleEventDescriptor, withReplyEvent reply: NSAppleEventDescriptor) {
|
||||
guard let text = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else { return }
|
||||
PlayerModel.shared.openStream(text)
|
||||
guard let url = URL(string: text) else { return }
|
||||
if url.scheme == "terebi-enqueue" { PlayerModel.shared.receiveEnqueueURL(url) }
|
||||
else { PlayerModel.shared.openStream(text) }
|
||||
}
|
||||
|
||||
@objc private func openDocuments(_ event: NSAppleEventDescriptor, withReplyEvent reply: NSAppleEventDescriptor) {
|
||||
@@ -3568,7 +3629,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
// Let SwiftUI create the initial window for a Finder/document launch.
|
||||
func application(_ application: NSApplication, open urls: [URL]) {
|
||||
if urls.allSatisfy(\.isFileURL) { PlayerModel.shared.openFiles(urls) }
|
||||
if urls.contains(where: { $0.scheme == "terebi-enqueue" }) {
|
||||
guard urls.count == 1, let url = urls.first else { return }
|
||||
PlayerModel.shared.receiveEnqueueURL(url)
|
||||
} else if urls.allSatisfy(\.isFileURL) { PlayerModel.shared.openFiles(urls) }
|
||||
else if let url = urls.first { PlayerModel.shared.open(url) }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user