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..424c0399 --- /dev/null +++ b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift @@ -0,0 +1,124 @@ +// +// 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(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 9bbbe00c..b9226523 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2643,7 +2643,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Swiftfin tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Swiftfin tvOS/Info.plist"; @@ -2652,7 +2652,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2673,7 +2673,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Swiftfin tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Swiftfin tvOS/Info.plist"; @@ -2682,7 +2682,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; @@ -2837,7 +2837,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2874,7 +2874,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; 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..2c090fd3 --- /dev/null +++ b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift @@ -0,0 +1,108 @@ +// +// 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(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..c85777b6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -437,6 +437,7 @@ struct VLCPlayerCompactOverlayView_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/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