// // Swiftfin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at https://mozilla.org/MPL/2.0/. // // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults import Factory import Foundation import JellyfinAPI import UIKit // TODO: strongly type errors extension MediaSourceInfo { func videoPlayerViewModel(with item: BaseItemDto, playSessionID: String) throws -> VideoPlayerViewModel { let userSession: UserSession! = Container.shared.currentUserSession() let playbackURL: URL let playMethod: PlayMethod if let transcodingURL { guard let fullTranscodeURL = userSession.client.fullURL(with: transcodingURL) else { throw JellyfinAPIError("Unable to make transcode URL") } playbackURL = fullTranscodeURL playMethod = .transcode } else { let videoStreamParameters = Paths.GetVideoStreamParameters( isStatic: true, tag: item.etag, playSessionID: playSessionID, mediaSourceID: id ) let videoStreamRequest = Paths.getVideoStream( itemID: item.id!, parameters: videoStreamParameters ) guard let streamURL = userSession.client.fullURL(with: videoStreamRequest) else { throw JellyfinAPIError("Unable to make stream URL") } playbackURL = streamURL playMethod = .directPlay } let videoStreams = mediaStreams?.filter { $0.type == .video } ?? [] let audioStreams = mediaStreams?.filter { $0.type == .audio } ?? [] let subtitleStreams = mediaStreams?.filter { $0.type == .subtitle } ?? [] return .init( playbackURL: playbackURL, item: item, mediaSource: self, playSessionID: playSessionID, videoStreams: videoStreams, audioStreams: audioStreams, subtitleStreams: subtitleStreams, selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1, selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1, chapters: item.fullChapterInfo, playMethod: playMethod ) } func liveVideoPlayerViewModel(with item: BaseItemDto, playSessionID: String) throws -> VideoPlayerViewModel { let userSession: UserSession! = Container.shared.currentUserSession() let playbackURL: URL let playMethod: PlayMethod print("🎬 liveVideoPlayerViewModel: Starting for item \(item.displayTitle)") print("🎬 Server URL: \(userSession.server.currentURL)") print("🎬 TranscodingURL: \(transcodingURL ?? "nil")") print("🎬 Path: \(self.path ?? "nil")") print("🎬 SupportsDirectPlay: \(self.isSupportsDirectPlay ?? false)") print("🎬 MediaSourceInfo ID: \(self.id ?? "nil")") print("🎬 MediaSourceInfo Name: \(self.name ?? "nil")") print("🎬 Container: \(self.container ?? "nil")") print("🎬 PlaySessionID: \(playSessionID)") print("🎬 LiveStreamID: \(self.liveStreamID ?? "nil")") print("🎬 OpenToken: \(self.openToken ?? "nil")") // For Live TV: Try direct Dispatcharr proxy URL FIRST (Jellyfin's endpoints are broken) if let path = self.path, let pathURL = URL(string: path), pathURL.scheme != nil { // Use direct Dispatcharr proxy stream (MPEG-TS over HTTP) playbackURL = pathURL playMethod = .directPlay print("🎬 Using direct Dispatcharr proxy path: \(playbackURL)") print("🎬 Absolute URL: \(playbackURL.absoluteString)") } else if let transcodingURL { // Fallback to Jellyfin transcoding URL (doesn't work for Dispatcharr channels) let liveTranscodingURL = transcodingURL.replacingOccurrences(of: "/master.m3u8", with: "/live.m3u8") guard var fullTranscodeURL = userSession.client.fullURL(with: liveTranscodingURL) else { throw JellyfinAPIError("Unable to make transcode URL") } // Add LiveStreamId parameter using URLComponents for proper encoding if let openToken = self.openToken, var components = URLComponents(url: fullTranscodeURL, resolvingAgainstBaseURL: false) { var queryItems = components.queryItems ?? [] queryItems.append(URLQueryItem(name: "LiveStreamId", value: openToken)) components.queryItems = queryItems if let urlWithLiveStreamId = components.url { fullTranscodeURL = urlWithLiveStreamId print("🎬 Added LiveStreamId parameter: \(openToken)") } } playbackURL = fullTranscodeURL playMethod = .transcode print("🎬 Using live transcoding URL (converted from master): \(playbackURL)") print("🎬 Absolute URL: \(playbackURL.absoluteString)") } else if false, let path = self.path, let pathURL = URL(string: path), pathURL.scheme != nil { // Direct path disabled - fails with AVPlayer connection error playbackURL = pathURL playMethod = .directPlay print("🎬 Using direct path URL (absolute): \(playbackURL)") print("🎬 Absolute URL: \(playbackURL.absoluteString)") } else if false, self.isSupportsDirectPlay ?? false, let path = self.path, let playbackUrl = URL(string: path) { // Relative direct play disabled playbackURL = playbackUrl playMethod = .directPlay print("🎬 Using direct play URL (relative): \(playbackURL)") print("🎬 Absolute URL: \(playbackURL.absoluteString)") } else { // Use Jellyfin's live.m3u8 endpoint for Live TV (same as web browser) // Construct URL: /videos/{id}/live.m3u8?DeviceId=...&MediaSourceId=...&PlaySessionId=...&api_key=... let deviceId = userSession.client.configuration.deviceID ?? "unknown" let apiKey = userSession.client.accessToken ?? "" var urlComponents = URLComponents() urlComponents.scheme = userSession.server.currentURL.scheme urlComponents.host = userSession.server.currentURL.host urlComponents.port = userSession.server.currentURL.port urlComponents.path = "/videos/\(item.id!)/live.m3u8" urlComponents.queryItems = [ URLQueryItem(name: "DeviceId", value: deviceId), URLQueryItem(name: "MediaSourceId", value: id), URLQueryItem(name: "PlaySessionId", value: playSessionID), URLQueryItem(name: "api_key", value: apiKey), ] guard let liveURL = urlComponents.url else { print("🎬 ERROR: Unable to construct live.m3u8 URL") throw JellyfinAPIError("Unable to construct live.m3u8 URL") } playbackURL = liveURL playMethod = .directPlay print("🎬 Using live.m3u8 URL: \(playbackURL)") print("🎬 Absolute URL: \(playbackURL.absoluteString)") } print("🎬 Final playback URL absolute string: \(playbackURL.absoluteString)") print("🎬 Play method: \(playMethod)") let videoStreams = mediaStreams?.filter { $0.type == .video } ?? [] let audioStreams = mediaStreams?.filter { $0.type == .audio } ?? [] let subtitleStreams = mediaStreams?.filter { $0.type == .subtitle } ?? [] return .init( playbackURL: playbackURL, item: item, mediaSource: self, playSessionID: playSessionID, videoStreams: videoStreams, audioStreams: audioStreams, subtitleStreams: subtitleStreams, selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1, selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1, chapters: item.fullChapterInfo, playMethod: playMethod ) } }