From 5b18c3c114054034227b0534391b74dcba889932 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 9 Nov 2025 14:04:28 +0100 Subject: [PATCH] Add multi-track audio support for Piped backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract and provide all available audio tracks (ORIGINAL, DUBBED, etc.) from Piped API instead of only using the first ORIGINAL track. This allows users to select between different audio languages and track types. Changes: - Extract all M4A audio tracks grouped by type and language - Keep highest bitrate stream for each unique track combination - Sort tracks with ORIGINAL first, then others alphabetically - Pass audio tracks array to Stream for player selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Model/Applications/PipedAPI.swift | 63 +++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index 001e6bbd..4a0c5def 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -680,20 +680,59 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { streams.append(Stream(instance: account.instance, hlsURL: hlsURL)) } - let audioStreams = content + // Extract all M4A audio streams, sorted by bitrate (highest first) + let allAudioStreams = content .dictionaryValue["audioStreams"]? .arrayValue .filter { $0.dictionaryValue["format"]?.string == "M4A" } - .filter { stream in - let type = stream.dictionaryValue["audioTrackType"]?.string - return type == nil || type == "ORIGINAL" - } .sorted { $0.dictionaryValue["bitrate"]?.int ?? 0 > $1.dictionaryValue["bitrate"]?.int ?? 0 } ?? [] - guard let audioStream = audioStreams.first else { + // Group audio streams by track type and language, keeping highest bitrate for each + var audioTracksByType = [String: JSON]() + for audioStream in allAudioStreams { + let trackType = audioStream.dictionaryValue["audioTrackType"]?.string + let trackLocale = audioStream.dictionaryValue["audioTrackLocale"]?.string + + // Create a unique key for this audio track combination + let key = "\(trackType ?? "ORIGINAL")_\(trackLocale ?? "")" + + // Only keep the first (highest bitrate) stream for each unique track type/locale combination + if audioTracksByType[key] == nil { + audioTracksByType[key] = audioStream + } + } + + // Convert to Stream.AudioTrack array + let audioTracks: [Stream.AudioTrack] = audioTracksByType.values.compactMap { audioStream in + guard let url = audioStream.dictionaryValue["url"]?.url else { + return nil + } + + let trackType = audioStream.dictionaryValue["audioTrackType"]?.string + let trackLocale = audioStream.dictionaryValue["audioTrackLocale"]?.string + + return Stream.AudioTrack( + url: url, + content: trackType, + language: trackLocale + ) + }.sorted { track1, track2 in + // Sort: ORIGINAL first, then DUBBED, then others + if track1.content == "ORIGINAL" && track2.content != "ORIGINAL" { + return true + } else if track1.content != "ORIGINAL" && track2.content == "ORIGINAL" { + return false + } else { + // If both are same type, sort by language + return (track1.language ?? "") < (track2.language ?? "") + } + } + + // Fallback to first audio stream if no tracks were extracted + guard !audioTracks.isEmpty else { return streams } @@ -705,13 +744,12 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { continue } - guard let audioAssetUrl = audioStream.dictionaryValue["url"]?.url, - let videoAssetUrl = videoStream.dictionaryValue["url"]?.url - else { + guard let videoAssetUrl = videoStream.dictionaryValue["url"]?.url else { continue } - let audioAsset = AVURLAsset(url: audioAssetUrl) + // Use the first (ORIGINAL) audio track as default + let defaultAudioAsset = AVURLAsset(url: audioTracks[0].url) let videoAsset = AVURLAsset(url: videoAssetUrl) let videoOnly = videoStream.dictionaryValue["videoOnly"]?.bool ?? true @@ -739,13 +777,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { streams.append( Stream( instance: account.instance, - audioAsset: audioAsset, + audioAsset: defaultAudioAsset, videoAsset: videoAsset, resolution: resolution, kind: .adaptive, videoFormat: videoFormat, bitrate: bitrate, - requestRange: requestRange + requestRange: requestRange, + audioTracks: audioTracks ) ) } else {