diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index d89aac15..8eb6e800 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -169,125 +169,156 @@ extension BaseItemDto { func createLiveTVVideoPlayerViewModel() -> AnyPublisher<[VideoPlayerViewModel], Error> { - LogManager.shared.log.debug("Creating liveTV video player view model for item: \(id ?? "")") - - let builder = DeviceProfileBuilder() - // TODO: fix bitrate settings - let tempOverkillBitrate = 360_000_000 - builder.setMaxBitrate(bitrate: tempOverkillBitrate) - let profile = builder.buildProfile() - - let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id, - maxStreamingBitrate: tempOverkillBitrate, - startTimeTicks: self.userData?.playbackPositionTicks ?? 0, - deviceProfile: profile, - autoOpenLiveStream: true) - - return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!, - userId: SessionManager.main.currentLogin.user.id, - maxStreamingBitrate: tempOverkillBitrate, - startTimeTicks: self.userData?.playbackPositionTicks ?? 0, - autoOpenLiveStream: true, - playbackInfoDto: playbackInfo) - .map { response -> [VideoPlayerViewModel] in - let mediaSources = response.mediaSources! - - var viewModels: [VideoPlayerViewModel] = [] - - for currentMediaSource in mediaSources { - let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] - let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] - - let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! }) - - let defaultSubtitleStream = subtitleStreams - .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) - - var directStreamURL: URL - let transcodedStreamURL: URLComponents? - let mediaSourceID: String - let streamType: ServerStreamType - - if let transcodeURL = currentMediaSource.transcodingUrl { - streamType = .transcode - transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI - .appending(transcodeURL))! - } else { - streamType = .direct - transcodedStreamURL = nil - } - - if mediaSources.count > 1 { - mediaSourceID = currentMediaSource.id! - } else { - mediaSourceID = self.id! - } - - let requestBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!, - _static: true, - tag: self.etag, - minSegments: 6, - mediaSourceId: mediaSourceID) - directStreamURL = URL(string: requestBuilder.URLString)! - - // MARK: VidoPlayerViewModel Creation - - var subtitle: String? - - // MARK: Attach media content to self - - var modifiedSelfItem = self - modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams - - // TODO: other forms of media subtitle - if self.itemType == .episode { - if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { - subtitle = "\(seriesName) - \(episodeLocator)" - } - } - - let subtitlesEnabled = defaultSubtitleStream != nil - - let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode - let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay - - let overlayType = Defaults[.overlayType] - - let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode - let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode - - var fileName: String? - if let lastInPath = currentMediaSource.path?.split(separator: "/").last { - fileName = String(lastInPath) - } - - let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem, - title: modifiedSelfItem.name ?? "", - subtitle: subtitle, - directStreamURL: directStreamURL, - transcodedStreamURL: transcodedStreamURL?.url, - streamType: streamType, - response: response, - audioStreams: audioStreams, - subtitleStreams: subtitleStreams, - chapters: modifiedSelfItem.chapters ?? [], - selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, - selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, - subtitlesEnabled: subtitlesEnabled, - autoplayEnabled: autoplayEnabled, - overlayType: overlayType, - shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, - shouldShowPlayNextItem: shouldShowPlayNextItem, - shouldShowAutoPlay: shouldShowAutoPlay, - container: currentMediaSource.container ?? "", - filename: fileName, - versionName: currentMediaSource.name) - - viewModels.append(videoPlayerViewModel) - } - - return viewModels - } - .eraseToAnyPublisher() + LogManager.shared.log.debug("Creating liveTV video player view model for item: \(id ?? "")") + + let builder = DeviceProfileBuilder() + // TODO: fix bitrate settings + let tempOverkillBitrate = 360_000_000 + builder.setMaxBitrate(bitrate: tempOverkillBitrate) + let profile = builder.buildProfile() + + let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id, + maxStreamingBitrate: tempOverkillBitrate, + startTimeTicks: self.userData?.playbackPositionTicks ?? 0, + deviceProfile: profile, + autoOpenLiveStream: true) + + return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!, + userId: SessionManager.main.currentLogin.user.id, + maxStreamingBitrate: tempOverkillBitrate, + startTimeTicks: self.userData?.playbackPositionTicks ?? 0, + autoOpenLiveStream: true, + playbackInfoDto: playbackInfo) + .map { response -> [VideoPlayerViewModel] in + let mediaSources = response.mediaSources! + + var viewModels: [VideoPlayerViewModel] = [] + + for currentMediaSource in mediaSources { + let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first + let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] + let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] + + let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! }) + + let defaultSubtitleStream = subtitleStreams + .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) + + // MARK: Build Streams + + let directStreamURL: URL + let transcodedStreamURL: URLComponents? + var hlsStreamURL: URL + let mediaSourceID: String + let streamType: ServerStreamType + + if mediaSources.count > 1 { + mediaSourceID = currentMediaSource.id! + } else { + mediaSourceID = self.id! + } + + let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!, + _static: true, + tag: self.etag, + playSessionId: response.playSessionId, + minSegments: 6, + mediaSourceId: mediaSourceID) + directStreamURL = URL(string: directStreamBuilder.URLString)! + + if let transcodeURL = currentMediaSource.transcodingUrl { + streamType = .transcode + transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI + .appending(transcodeURL))! + } else { + streamType = .direct + transcodedStreamURL = nil + } + + let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "", + mediaSourceId: id ?? "", + _static: true, + tag: currentMediaSource.eTag, + deviceProfileId: nil, + playSessionId: response.playSessionId, + segmentContainer: "ts", + segmentLength: nil, + minSegments: 2, + deviceId: UIDevice.vendorUUIDString, + audioCodec: audioStreams + .compactMap(\.codec) + .joined(separator: ","), + breakOnNonKeyFrames: true, + requireAvc: true, + transcodingMaxAudioChannels: 6, + videoCodec: videoStream?.codec, + videoStreamIndex: videoStream?.index, + enableAdaptiveBitrateStreaming: true) + + var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! + hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) + + hlsStreamURL = hlsStreamComponents.url! + + // MARK: VidoPlayerViewModel Creation + + var subtitle: String? + + // MARK: Attach media content to self + + var modifiedSelfItem = self + modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams + + // TODO: other forms of media subtitle + if self.itemType == .episode { + if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { + subtitle = "\(seriesName) - \(episodeLocator)" + } + } + + let subtitlesEnabled = defaultSubtitleStream != nil + + let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode + let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay + + let overlayType = Defaults[.overlayType] + + let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode + let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode + + var fileName: String? + if let lastInPath = currentMediaSource.path?.split(separator: "/").last { + fileName = String(lastInPath) + } + + let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem, + title: modifiedSelfItem.name ?? "", + subtitle: subtitle, + directStreamURL: directStreamURL, + transcodedStreamURL: transcodedStreamURL?.url, + hlsStreamURL: hlsStreamURL, + streamType: streamType, + response: response, + audioStreams: audioStreams, + subtitleStreams: subtitleStreams, + chapters: modifiedSelfItem.chapters ?? [], + selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, + selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, + subtitlesEnabled: subtitlesEnabled, + autoplayEnabled: autoplayEnabled, + overlayType: overlayType, + shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, + shouldShowPlayNextItem: shouldShowPlayNextItem, + shouldShowAutoPlay: shouldShowAutoPlay, + container: currentMediaSource.container ?? "", + filename: fileName, + versionName: currentMediaSource.name) + + viewModels.append(videoPlayerViewModel) + } + + return viewModels + } + .eraseToAnyPublisher() } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift index 66ac0b81..efad68a4 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift @@ -142,7 +142,8 @@ struct tvOSLiveTVOverlay_Previews: PreviewProvider { title: "Glorious Purpose", subtitle: "Loki - S1E1", directStreamURL: URL(string: "www.apple.com")!, - transcodedStreamURL: nil, + transcodedStreamURL: nil, + hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)],