add back experimental native player
This commit is contained in:
parent
433d4a97be
commit
961f87d3c7
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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)],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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)],
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue