jellyflood/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPl...

179 lines
8.0 KiB
Swift

//
// 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
)
}
}