mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
Apply audio mode to downloaded videos
Downloads bypass stream selection and reach play() as ready-made local file streams, so audio mode never affected them. Apply the mode at that choke point instead: swap a local video stream for the separately downloaded audio track, or play a muxed file with the video track disabled (vid set per-load in MPVClient, never toggled live). The transform is bidirectional so stored audio-only queue/history entries restore full video when the mode is off, and setAudioMode now reloads downloaded content at the current position in both directions.
This commit is contained in:
@@ -189,6 +189,38 @@ struct StreamResolution: Codable, Hashable, Sendable, Comparable, CustomStringCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Audio-Only Variant
|
||||||
|
|
||||||
|
extension Stream {
|
||||||
|
/// Whether the backend must disable the video track when loading this stream:
|
||||||
|
/// a muxed file being played as audio-only (see `audioOnlyVariant()`).
|
||||||
|
var requiresVideoTrackDisabled: Bool {
|
||||||
|
isAudioOnly && videoCodec != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an audio-only copy of this stream keeping the same URL.
|
||||||
|
/// Used for muxed local files in audio mode: `videoCodec` stays set so
|
||||||
|
/// `requiresVideoTrackDisabled` tells the backend to skip the video track.
|
||||||
|
func audioOnlyVariant() -> Stream {
|
||||||
|
Stream(
|
||||||
|
url: url,
|
||||||
|
resolution: nil,
|
||||||
|
format: format,
|
||||||
|
videoCodec: videoCodec,
|
||||||
|
audioCodec: audioCodec,
|
||||||
|
bitrate: bitrate,
|
||||||
|
fileSize: fileSize,
|
||||||
|
isAudioOnly: true,
|
||||||
|
isLive: isLive,
|
||||||
|
mimeType: mimeType,
|
||||||
|
audioLanguage: audioLanguage,
|
||||||
|
audioTrackName: audioTrackName,
|
||||||
|
isOriginalAudio: isOriginalAudio,
|
||||||
|
httpHeaders: httpHeaders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - URL Rewriting
|
// MARK: - URL Rewriting
|
||||||
|
|
||||||
extension Stream {
|
extension Stream {
|
||||||
|
|||||||
@@ -621,13 +621,24 @@ final class MPVClient: @unchecked Sendable {
|
|||||||
/// - audioURL: Optional separate audio track URL (for video-only streams)
|
/// - audioURL: Optional separate audio track URL (for video-only streams)
|
||||||
/// - httpHeaders: Optional HTTP headers for streaming (cookies, referer, etc.)
|
/// - httpHeaders: Optional HTTP headers for streaming (cookies, referer, etc.)
|
||||||
/// - useEDL: If true and audioURL is provided, combine streams using EDL for unified caching
|
/// - useEDL: If true and audioURL is provided, combine streams using EDL for unified caching
|
||||||
|
/// - disableVideoTrack: If true, load with the video track disabled (audio mode for muxed files)
|
||||||
/// - options: Additional MPV options
|
/// - options: Additional MPV options
|
||||||
func loadFile(_ url: URL, audioURL: URL? = nil, httpHeaders: [String: String]? = nil, useEDL: Bool = true, options: [String] = []) throws {
|
func loadFile(_ url: URL, audioURL: URL? = nil, httpHeaders: [String: String]? = nil, useEDL: Bool = true, disableVideoTrack: Bool = false, options: [String] = []) throws {
|
||||||
try mpvQueue.sync {
|
try mpvQueue.sync {
|
||||||
guard mpv != nil, !isDestroyed else {
|
guard mpv != nil, !isDestroyed else {
|
||||||
throw MPVError.commandFailed("loadfile")
|
throw MPVError.commandFailed("loadfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video track selection for the upcoming file. Set as a property
|
||||||
|
// before the loadfile command (per-file loadfile options moved to
|
||||||
|
// the 4th argument in mpv 0.38+, so they can't be relied on here).
|
||||||
|
// Only ever changed between loads, never live - live `vid` toggling
|
||||||
|
// causes A/V desync (see handlePlayerSheetVisibility).
|
||||||
|
setPropertyUnsafe("vid", disableVideoTrack ? "no" : "auto")
|
||||||
|
if disableVideoTrack {
|
||||||
|
logDebug("Video track disabled for this load (audio mode)")
|
||||||
|
}
|
||||||
|
|
||||||
// Set HTTP headers as a property before loading (if provided)
|
// Set HTTP headers as a property before loading (if provided)
|
||||||
// Use MPV_FORMAT_NODE_ARRAY for proper header handling
|
// Use MPV_FORMAT_NODE_ARRAY for proper header handling
|
||||||
if let httpHeaders, !httpHeaders.isEmpty {
|
if let httpHeaders, !httpHeaders.isEmpty {
|
||||||
|
|||||||
@@ -495,7 +495,13 @@ final class MPVBackend: PlayerBackend {
|
|||||||
|
|
||||||
// Load the stream
|
// Load the stream
|
||||||
do {
|
do {
|
||||||
try mpvClient?.loadFile(stream.url, audioURL: audioStream?.url, httpHeaders: stream.httpHeaders, useEDL: useEDL)
|
try mpvClient?.loadFile(
|
||||||
|
stream.url,
|
||||||
|
audioURL: audioStream?.url,
|
||||||
|
httpHeaders: stream.httpHeaders,
|
||||||
|
useEDL: useEDL,
|
||||||
|
disableVideoTrack: stream.requiresVideoTrackDisabled
|
||||||
|
)
|
||||||
|
|
||||||
// Give MPV a moment to process the loadfile command
|
// Give MPV a moment to process the loadfile command
|
||||||
try await Task.sleep(for: .milliseconds(100))
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|||||||
@@ -203,6 +203,14 @@ final class PlayerService {
|
|||||||
/// - audioStream: Optional separate audio stream (for video-only streams)
|
/// - audioStream: Optional separate audio stream (for video-only streams)
|
||||||
/// - startTime: Optional start time in seconds
|
/// - startTime: Optional start time in seconds
|
||||||
func play(video: Video, stream: Stream? = nil, audioStream: Stream? = nil, startTime: TimeInterval? = nil) async {
|
func play(video: Video, stream: Stream? = nil, audioStream: Stream? = nil, startTime: TimeInterval? = nil) async {
|
||||||
|
// Downloaded/local files bypass stream selection (they arrive here as
|
||||||
|
// ready-made file:// streams), so audio mode is applied at this choke
|
||||||
|
// point instead of in selectStreams.
|
||||||
|
var stream = stream
|
||||||
|
var audioStream = audioStream
|
||||||
|
if let provided = stream {
|
||||||
|
(stream, audioStream) = applyingAudioModeToLocalStreams(video: video, stream: provided, audioStream: audioStream)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up audio session when playback actually starts (not at app launch)
|
// Set up audio session when playback actually starts (not at app launch)
|
||||||
setupAudioSession()
|
setupAudioSession()
|
||||||
@@ -1378,6 +1386,37 @@ final class PlayerService {
|
|||||||
await play(video: video, stream: stream, audioStream: audioStream, startTime: currentTime)
|
await play(video: video, stream: stream, audioStream: audioStream, startTime: currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies audio-only mode to local (downloaded) streams. Online streams
|
||||||
|
/// are handled by stream selection; local files reach `play()` pre-resolved,
|
||||||
|
/// so the swap happens here instead.
|
||||||
|
private func applyingAudioModeToLocalStreams(video: Video, stream: Stream, audioStream: Stream?) -> (Stream, Stream?) {
|
||||||
|
guard stream.url.isFileURL else { return (stream, audioStream) }
|
||||||
|
|
||||||
|
if settingsManager?.audioOnlyModeEnabled == true {
|
||||||
|
guard !stream.isAudioOnly else { return (stream, audioStream) }
|
||||||
|
if let audioStream, audioStream.url.isFileURL {
|
||||||
|
// Separate downloaded audio track - play it on its own
|
||||||
|
LoggingService.shared.logPlayer("Audio mode: using downloaded audio track for \(video.id.id)")
|
||||||
|
return (audioStream, nil)
|
||||||
|
}
|
||||||
|
// Muxed file - same URL with the video track disabled at load time
|
||||||
|
LoggingService.shared.logPlayer("Audio mode: playing muxed local file without video track for \(video.id.id)")
|
||||||
|
return (stream.audioOnlyVariant(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode is off but an audio-only stream was stored for a downloaded
|
||||||
|
// video (e.g. queue/history item created while audio mode was on) -
|
||||||
|
// restore the full local streams from the download record.
|
||||||
|
if stream.isAudioOnly,
|
||||||
|
let downloadManager,
|
||||||
|
let download = downloadManager.download(for: video.id),
|
||||||
|
download.status == .completed,
|
||||||
|
let (_, videoStream, downloadAudio, _, _) = downloadManager.videoAndStream(for: download) {
|
||||||
|
return (videoStream, downloadAudio)
|
||||||
|
}
|
||||||
|
return (stream, audioStream)
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggles global audio-only ("music") mode. If an online video is playing,
|
/// Toggles global audio-only ("music") mode. If an online video is playing,
|
||||||
/// reloads it at the current position (same mechanism as a quality switch).
|
/// reloads it at the current position (same mechanism as a quality switch).
|
||||||
func setAudioMode(_ enabled: Bool) async {
|
func setAudioMode(_ enabled: Bool) async {
|
||||||
@@ -1385,11 +1424,27 @@ final class PlayerService {
|
|||||||
settingsManager?.audioOnlyModeEnabled = enabled
|
settingsManager?.audioOnlyModeEnabled = enabled
|
||||||
LoggingService.shared.logPlayer("Audio mode \(enabled ? "enabled" : "disabled")")
|
LoggingService.shared.logPlayer("Audio mode \(enabled ? "enabled" : "disabled")")
|
||||||
|
|
||||||
// Nothing playing, no stream list to reselect from (e.g. still loading),
|
// Nothing playing: just persist the setting.
|
||||||
// or playing a downloaded/local file: just persist the setting.
|
guard state.currentVideo != nil else { return }
|
||||||
guard let video = state.currentVideo,
|
|
||||||
currentDownload == nil,
|
// Downloaded content: re-derive the local streams from the download
|
||||||
!availableStreams.isEmpty else { return }
|
// record and reload; play() applies the mode swap for local files
|
||||||
|
// (separate audio track, or same file with the video track disabled).
|
||||||
|
if let currentDownload {
|
||||||
|
guard (state.currentStream?.isAudioOnly == true) != enabled,
|
||||||
|
let downloadManager,
|
||||||
|
let (downloadedVideo, localStream, localAudio, captionURL, _) = downloadManager.videoAndStream(for: currentDownload)
|
||||||
|
else { return }
|
||||||
|
let currentTime = state.currentTime
|
||||||
|
await play(video: downloadedVideo, stream: localStream, audioStream: localAudio, startTime: currentTime)
|
||||||
|
if let captionURL {
|
||||||
|
loadLocalCaption(url: captionURL)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No stream list to reselect from (e.g. still loading): just persist.
|
||||||
|
guard let video = state.currentVideo, !availableStreams.isEmpty else { return }
|
||||||
|
|
||||||
let currentTime = state.currentTime
|
let currentTime = state.currentTime
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user