Merge pull request #320 from LePips/reimplement-native-player

This commit is contained in:
Ethan Pippin 2022-01-20 20:02:16 -07:00 committed by GitHub
commit 0a8d2a0112
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 370 additions and 34 deletions

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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,

View File

@ -72,6 +72,7 @@ extension Defaults.Keys {
suite: SwiftfinStore.Defaults.generalSuite)
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// tvos specific

View File

@ -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

View File

@ -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
}

View File

@ -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<AnyCancellable>()
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()
}
}

View File

@ -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)],

View File

@ -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

View File

@ -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 = "<group>"; };
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = "<group>"; };
E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = "<group>"; };
@ -861,6 +865,7 @@
E17885A7278130690094FBCF /* Overlays */,
E1C812C8277AE40900918266 /* VideoPlayerView.swift */,
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */,
E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */,
);
path = VideoPlayer;
sourceTree = "<group>";
@ -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 */,

View File

@ -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
}

View File

@ -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<AnyCancellable>()
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()
}
}

View File

@ -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]

View File

@ -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