mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 05:42:04 +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
|
||||
|
||||
extension Stream {
|
||||
|
||||
@@ -621,13 +621,24 @@ final class MPVClient: @unchecked Sendable {
|
||||
/// - audioURL: Optional separate audio track URL (for video-only streams)
|
||||
/// - httpHeaders: Optional HTTP headers for streaming (cookies, referer, etc.)
|
||||
/// - 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
|
||||
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 {
|
||||
guard mpv != nil, !isDestroyed else {
|
||||
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)
|
||||
// Use MPV_FORMAT_NODE_ARRAY for proper header handling
|
||||
if let httpHeaders, !httpHeaders.isEmpty {
|
||||
|
||||
@@ -495,7 +495,13 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
// Load the stream
|
||||
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
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
@@ -203,6 +203,14 @@ final class PlayerService {
|
||||
/// - audioStream: Optional separate audio stream (for video-only streams)
|
||||
/// - startTime: Optional start time in seconds
|
||||
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)
|
||||
setupAudioSession()
|
||||
@@ -1378,6 +1386,37 @@ final class PlayerService {
|
||||
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,
|
||||
/// reloads it at the current position (same mechanism as a quality switch).
|
||||
func setAudioMode(_ enabled: Bool) async {
|
||||
@@ -1385,11 +1424,27 @@ final class PlayerService {
|
||||
settingsManager?.audioOnlyModeEnabled = enabled
|
||||
LoggingService.shared.logPlayer("Audio mode \(enabled ? "enabled" : "disabled")")
|
||||
|
||||
// Nothing playing, no stream list to reselect from (e.g. still loading),
|
||||
// or playing a downloaded/local file: just persist the setting.
|
||||
guard let video = state.currentVideo,
|
||||
currentDownload == nil,
|
||||
!availableStreams.isEmpty else { return }
|
||||
// Nothing playing: just persist the setting.
|
||||
guard state.currentVideo != nil else { return }
|
||||
|
||||
// Downloaded content: re-derive the local streams from the download
|
||||
// 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user