diff --git a/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift index e193b581..94efc82b 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift @@ -28,12 +28,21 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { PreferenceUIHostingControllerView { - VLCPlayerView(viewModel: self.viewModel) - .navigationBarHidden(true) - .statusBar(hidden: true) - .ignoresSafeArea() - .prefersHomeIndicatorAutoHidden(true) - .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) + if Defaults[.Experimental.nativePlayer] { + NativePlayerView(viewModel: self.viewModel) + .navigationBarHidden(true) + .statusBar(hidden: true) + .ignoresSafeArea() + .prefersHomeIndicatorAutoHidden(true) + .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) + } else { + VLCPlayerView(viewModel: self.viewModel) + .navigationBarHidden(true) + .statusBar(hidden: true) + .ignoresSafeArea() + .prefersHomeIndicatorAutoHidden(true) + .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) + } }.ignoresSafeArea() } } diff --git a/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift index e01e279f..97136ed7 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift @@ -27,8 +27,14 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { - VLCPlayerView(viewModel: viewModel) - .navigationBarHidden(true) - .ignoresSafeArea() + if Defaults[.Experimental.nativePlayer] { + NativePlayerView(viewModel: viewModel) + .navigationBarHidden(true) + .ignoresSafeArea() + } else { + VLCPlayerView(viewModel: viewModel) + .navigationBarHidden(true) + .ignoresSafeArea() + } } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 8b743846..c658e9e1 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -40,6 +40,7 @@ extension BaseItemDto { 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 } ?? [] @@ -48,11 +49,28 @@ extension BaseItemDto { let defaultSubtitleStream = subtitleStreams .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) - var directStreamURL: URL + // 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 @@ -62,18 +80,30 @@ extension BaseItemDto { transcodedStreamURL = nil } - if mediaSources.count > 1 { - mediaSourceID = currentMediaSource.id! - } else { - mediaSourceID = self.id! - } + 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) - let requestBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!, - _static: true, - tag: self.etag, - minSegments: 6, - mediaSourceId: mediaSourceID) - directStreamURL = URL(string: requestBuilder.URLString)! + var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! + hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) + + hlsStreamURL = hlsStreamComponents.url! // MARK: VidoPlayerViewModel Creation @@ -111,6 +141,7 @@ extension BaseItemDto { subtitle: subtitle, directStreamURL: directStreamURL, transcodedStreamURL: transcodedStreamURL?.url, + hlsStreamURL: hlsStreamURL, streamType: streamType, response: response, audioStreams: audioStreams, diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 35bfa228..3cf1480c 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -72,6 +72,7 @@ extension Defaults.Keys { suite: SwiftfinStore.Defaults.generalSuite) static let forceDirectPlay = Key("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let liveTVAlphaEnabled = Key("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let nativePlayer = Key("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) } // tvos specific diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 1556f745..6ce29063 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -109,6 +109,7 @@ final class VideoPlayerViewModel: ViewModel { let subtitle: String? let directStreamURL: URL let transcodedStreamURL: URL? + let hlsStreamURL: URL let audioStreams: [MediaStream] let subtitleStreams: [MediaStream] let chapters: [ChapterInfo] @@ -147,6 +148,13 @@ final class VideoPlayerViewModel: ViewModel { Int64(currentSeconds) * 10_000_000 } + func setSeconds(_ seconds: Int64) { + let videoDuration = item.runTimeTicks! + let percentage = Double(seconds * 10_000_000) / Double(videoDuration) + + sliderPercentage = percentage + } + // MARK: Helpers var currentAudioStream: MediaStream? { @@ -188,6 +196,7 @@ final class VideoPlayerViewModel: ViewModel { subtitle: String?, directStreamURL: URL, transcodedStreamURL: URL?, + hlsStreamURL: URL, streamType: ServerStreamType, response: PlaybackInfoResponse, audioStreams: [MediaStream], @@ -210,6 +219,7 @@ final class VideoPlayerViewModel: ViewModel { self.subtitle = subtitle self.directStreamURL = directStreamURL self.transcodedStreamURL = transcodedStreamURL + self.hlsStreamURL = hlsStreamURL self.streamType = streamType self.response = response self.audioStreams = audioStreams diff --git a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift index b53b52aa..ab5c1b39 100644 --- a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift @@ -17,6 +17,8 @@ struct ExperimentalSettingsView: View { var syncSubtitleStateWithAdjacent @Default(.Experimental.liveTVAlphaEnabled) var liveTVAlphaEnabled + @Default(.Experimental.nativePlayer) + var nativePlayer var body: some View { Form { @@ -28,6 +30,8 @@ struct ExperimentalSettingsView: View { Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled) + Toggle("Native Player", isOn: $nativePlayer) + } header: { L10n.experimental.text } diff --git a/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift new file mode 100644 index 00000000..d0ffba8c --- /dev/null +++ b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift @@ -0,0 +1,130 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import AVKit +import Combine +import JellyfinAPI +import UIKit + +class NativePlayerViewController: AVPlayerViewController { + + let viewModel: VideoPlayerViewModel + + var timeObserverToken: Any? + + var lastProgressTicks: Int64 = 0 + + private var cancellables = Set() + + init(viewModel: VideoPlayerViewModel) { + + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + let player: AVPlayer + + if let transcodedStreamURL = viewModel.transcodedStreamURL { + player = AVPlayer(url: transcodedStreamURL) + } else { + player = AVPlayer(url: viewModel.hlsStreamURL) + } + + player.appliesMediaSelectionCriteriaAutomatically = false + player.currentItem?.externalMetadata = createMetadata() + + let timeScale = CMTimeScale(NSEC_PER_SEC) + let time = CMTime(seconds: 5, preferredTimescale: timeScale) + + timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + if time.seconds != 0 { + self?.sendProgressReport(seconds: time.seconds) + } + } + + self.player = player + + self.allowsPictureInPicturePlayback = true + self.player?.allowsExternalPlayback = true + } + + private func createMetadata() -> [AVMetadataItem] { + let allMetadata: [AVMetadataIdentifier: Any] = [ + .commonIdentifierTitle: viewModel.title, + .iTunesMetadataTrackSubTitle: viewModel.subtitle ?? "", + // Need to fix against an image that doesn't exist +// .commonIdentifierArtwork: UIImage(data: try! Data(contentsOf: viewModel.item.getBackdropImage(maxWidth: 200)))? +// .pngData() as Any, +// .commonIdentifierDescription: viewModel.item.overview ?? "", +// .iTunesMetadataContentRating: viewModel.item.officialRating ?? "", +// .quickTimeMetadataGenre: viewModel.item.genres?.first ?? "", + ] + + return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) } + } + + private func createMetadataItem(for identifier: AVMetadataIdentifier, + value: Any) -> AVMetadataItem + { + let item = AVMutableMetadataItem() + item.identifier = identifier + item.value = value as? NSCopying & NSObjectProtocol + // Specify "und" to indicate an undefined language. + item.extendedLanguageTag = "und" + return item.copy() as! AVMetadataItem + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + stop() + removePeriodicTimeObserver() + } + + func removePeriodicTimeObserver() { + if let timeObserverToken = timeObserverToken { + player?.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000), + toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1), + completionHandler: { _ in + self.play() + }) + } + + private func play() { + player?.play() + + viewModel.sendPlayReport() + } + + private func sendProgressReport(seconds: Double) { + viewModel.setSeconds(Int64(seconds)) + viewModel.sendProgressReport() + } + + private func stop() { + self.player?.pause() + viewModel.sendStopReport() + } +} diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift index 12d83836..20db21e6 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift @@ -143,6 +143,7 @@ struct tvOSVLCOverlay_Previews: PreviewProvider { subtitle: "Loki - S1E1", directStreamURL: URL(string: "www.apple.com")!, transcodedStreamURL: nil, + hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], diff --git a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift index a9684d8f..9a2908db 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift @@ -9,6 +9,20 @@ import SwiftUI import UIKit +struct NativePlayerView: UIViewControllerRepresentable { + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = NativePlayerViewController + + func makeUIViewController(context: Context) -> NativePlayerViewController { + + NativePlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} +} + struct VLCPlayerView: UIViewControllerRepresentable { let viewModel: VideoPlayerViewModel diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2e5f16ac..b3dc989d 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -290,6 +290,8 @@ E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; + E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */; }; + E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */; }; E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; }; E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; @@ -681,6 +683,8 @@ E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; + E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; + E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = ""; }; E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = ""; }; @@ -861,6 +865,7 @@ E17885A7278130690094FBCF /* Overlays */, E1C812C8277AE40900918266 /* VideoPlayerView.swift */, E1384943278036C70024FB48 /* VLCPlayerViewController.swift */, + E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */, ); path = VideoPlayer; sourceTree = ""; @@ -1565,8 +1570,9 @@ E193D5452719418B00900D82 /* VideoPlayer */ = { isa = PBXGroup; children = ( - E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, + E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */, E1002B692793E12E00E47059 /* Overlays */, + E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */, E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */, ); @@ -2249,6 +2255,7 @@ C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, + E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, @@ -2339,6 +2346,7 @@ 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, diff --git a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift index 271cb887..57c5ca7b 100644 --- a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift @@ -15,6 +15,8 @@ struct ExperimentalSettingsView: View { var forceDirectPlay @Default(.Experimental.syncSubtitleStateWithAdjacent) var syncSubtitleStateWithAdjacent + @Default(.Experimental.nativePlayer) + var nativePlayer var body: some View { Form { @@ -24,6 +26,8 @@ struct ExperimentalSettingsView: View { Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent) + Toggle("Native Player", isOn: $nativePlayer) + } header: { L10n.experimental.text } diff --git a/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift new file mode 100644 index 00000000..07e31d5c --- /dev/null +++ b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift @@ -0,0 +1,114 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import AVKit +import Combine +import JellyfinAPI +import UIKit + +class NativePlayerViewController: AVPlayerViewController { + + let viewModel: VideoPlayerViewModel + + var timeObserverToken: Any? + + var lastProgressTicks: Int64 = 0 + + private var cancellables = Set() + + init(viewModel: VideoPlayerViewModel) { + + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + let player: AVPlayer + + if let transcodedStreamURL = viewModel.transcodedStreamURL { + player = AVPlayer(url: transcodedStreamURL) + } else { + player = AVPlayer(url: viewModel.hlsStreamURL) + } + + player.appliesMediaSelectionCriteriaAutomatically = false + + let timeScale = CMTimeScale(NSEC_PER_SEC) + let time = CMTime(seconds: 5, preferredTimescale: timeScale) + + timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + if time.seconds != 0 { + self?.sendProgressReport(seconds: time.seconds) + } + } + + self.player = player + + self.allowsPictureInPicturePlayback = true + self.player?.allowsExternalPlayback = true + } + + private func createMetadataItem(for identifier: AVMetadataIdentifier, + value: Any) -> AVMetadataItem + { + let item = AVMutableMetadataItem() + item.identifier = identifier + item.value = value as? NSCopying & NSObjectProtocol + // Specify "und" to indicate an undefined language. + item.extendedLanguageTag = "und" + return item.copy() as! AVMetadataItem + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + stop() + removePeriodicTimeObserver() + } + + func removePeriodicTimeObserver() { + if let timeObserverToken = timeObserverToken { + player?.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000), + toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1), + completionHandler: { _ in + self.play() + }) + } + + private func play() { + player?.play() + + viewModel.sendPlayReport() + } + + private func sendProgressReport(seconds: Double) { + viewModel.setSeconds(Int64(seconds)) + viewModel.sendProgressReport() + } + + private func stop() { + self.player?.pause() + viewModel.sendStopReport() + } +} diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index 08133616..9bb57530 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -14,7 +14,6 @@ import Sliders import SwiftUI struct VLCPlayerOverlayView: View { - @ObservedObject var viewModel: VideoPlayerViewModel @@ -49,11 +48,9 @@ struct VLCPlayerOverlayView: View { @ViewBuilder private var mainBody: some View { VStack { - // MARK: Top Bar - ZStack(alignment: .center) { - + ZStack(alignment: .top) { if viewModel.overlayType == .compact { LinearGradient(gradient: Gradient(colors: [.black.opacity(0.8), .clear]), startPoint: .top, @@ -63,9 +60,7 @@ struct VLCPlayerOverlayView: View { } VStack(alignment: .EpisodeSeriesAlignmentGuide) { - HStack(alignment: .center) { - HStack { Button { viewModel.playerOverlayDelegate?.didSelectClose() @@ -87,7 +82,6 @@ struct VLCPlayerOverlayView: View { Spacer() HStack(spacing: 20) { - // MARK: Previous Item if viewModel.shouldShowPlayPreviousItem { @@ -165,7 +159,6 @@ struct VLCPlayerOverlayView: View { // MARK: Settings Menu Menu { - // MARK: Audio Streams Menu { @@ -337,7 +330,6 @@ struct VLCPlayerOverlayView: View { // MARK: Bottom Bar ZStack(alignment: .center) { - if viewModel.overlayType == .compact { LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8)]), startPoint: .top, @@ -347,7 +339,6 @@ struct VLCPlayerOverlayView: View { } HStack { - if viewModel.overlayType == .compact { HStack { Button { @@ -431,12 +422,12 @@ struct VLCPlayerOverlayView: View { } struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { - static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(), title: "Glorious Purpose", subtitle: "Loki - S1E1", directStreamURL: URL(string: "www.apple.com")!, transcodedStreamURL: nil, + hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], @@ -468,7 +459,6 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { // MARK: TitleSubtitleAlignment extension HorizontalAlignment { - private struct TitleSubtitleAlignment: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context[HorizontalAlignment.leading] diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift index a9684d8f..9a2908db 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift @@ -9,6 +9,20 @@ import SwiftUI import UIKit +struct NativePlayerView: UIViewControllerRepresentable { + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = NativePlayerViewController + + func makeUIViewController(context: Context) -> NativePlayerViewController { + + NativePlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} +} + struct VLCPlayerView: UIViewControllerRepresentable { let viewModel: VideoPlayerViewModel