diff --git a/Example/VLCUIExample/.DS_Store b/Example/VLCUIExample/.DS_Store index 8bbbaee..c2095e1 100644 Binary files a/Example/VLCUIExample/.DS_Store and b/Example/VLCUIExample/.DS_Store differ diff --git a/Example/VLCUIExample/Shared/ContentView.swift b/Example/VLCUIExample/Shared/ContentView.swift index 47d61b4..9539bfb 100644 --- a/Example/VLCUIExample/Shared/ContentView.swift +++ b/Example/VLCUIExample/Shared/ContentView.swift @@ -9,7 +9,7 @@ struct ContentView: View { var body: some View { ZStack(alignment: .bottom) { VLCVideoPlayer(configuration: viewModel.configuration) - .eventSubject(viewModel.eventSubject) + .proxy(viewModel.proxy) .onTicksUpdated { ticks, playbackInformation in viewModel.ticks = ticks viewModel.totalTicks = playbackInformation.length diff --git a/Example/VLCUIExample/Shared/ContentViewModel.swift b/Example/VLCUIExample/Shared/ContentViewModel.swift index 327f111..8f80e71 100644 --- a/Example/VLCUIExample/Shared/ContentViewModel.swift +++ b/Example/VLCUIExample/Shared/ContentViewModel.swift @@ -13,7 +13,7 @@ class ContentViewModel: ObservableObject { @Published var totalTicks: Int32 = 0 - var eventSubject: CurrentValueSubject = .init(nil) + let proxy: VLCVideoPlayer.Proxy = .init() var configuration: VLCVideoPlayer.Configuration { let configuration = VLCVideoPlayer @@ -30,7 +30,7 @@ class ContentViewModel: ObservableObject { ((totalTicks.roundDownNearestThousand - ticks.roundDownNearestThousand) / 1000).timeLabel } - func setCustomPosition(_ position: Float) { - self.position = position + func setCustomPosition(_ newPosition: Float) { + position = newPosition } } diff --git a/Example/VLCUIExample/Shared/IntExtensions.swift b/Example/VLCUIExample/Shared/IntExtensions.swift index 8aa5d86..7c7ace8 100644 --- a/Example/VLCUIExample/Shared/IntExtensions.swift +++ b/Example/VLCUIExample/Shared/IntExtensions.swift @@ -1,6 +1,7 @@ import Foundation extension Int32 { + var timeLabel: String { let formatter = DateComponentsFormatter() formatter.unitsStyle = .abbreviated diff --git a/Example/VLCUIExample/iOS/OverlayView.swift b/Example/VLCUIExample/iOS/OverlayView.swift index 31c0e43..238622d 100644 --- a/Example/VLCUIExample/iOS/OverlayView.swift +++ b/Example/VLCUIExample/iOS/OverlayView.swift @@ -14,7 +14,7 @@ struct OverlayView: View { HStack(spacing: 20) { Button { - viewModel.eventSubject.send(.jumpBackward(15)) + viewModel.proxy.jumpBackward(15) } label: { Image(systemName: "gobackward.15") .font(.system(size: 28, weight: .regular, design: .default)) @@ -22,9 +22,9 @@ struct OverlayView: View { Button { if viewModel.playerState == .playing { - viewModel.eventSubject.send(.pause) + viewModel.proxy.pause() } else { - viewModel.eventSubject.send(.play) + viewModel.proxy.play() } } label: { Group { @@ -41,14 +41,14 @@ struct OverlayView: View { } Button { - viewModel.eventSubject.send(.jumpForward(15)) + viewModel.proxy.jumpForward(15) } label: { Image(systemName: "goforward.15") .font(.system(size: 28, weight: .regular, design: .default)) } HStack(spacing: 5) { - Text((viewModel.ticks.roundDownNearestThousand / 1000).timeLabel) + Text(viewModel.positiveTimeLabel) .frame(width: 50) Slider( @@ -58,17 +58,17 @@ struct OverlayView: View { isScrubbing = isEditing } - Text(((viewModel.totalTicks.roundDownNearestThousand - viewModel.ticks.roundDownNearestThousand) / 1000).timeLabel) + Text(viewModel.negativeTimeLabel) .frame(width: 50) } } .onChange(of: isScrubbing) { isScrubbing in guard !isScrubbing else { return } - self.viewModel.eventSubject.send(.setTime(.ticks(viewModel.totalTicks * Int32(currentPosition * 100) / 100))) + viewModel.proxy.setTime(.ticks(viewModel.totalTicks * Int32(currentPosition * 100) / 100)) } .onChange(of: viewModel.position) { newValue in guard !isScrubbing else { return } - self.currentPosition = newValue + currentPosition = newValue } } } diff --git a/Example/VLCUIExample/macOS/OverlayView.swift b/Example/VLCUIExample/macOS/OverlayView.swift index 857d11e..1df9a90 100644 --- a/Example/VLCUIExample/macOS/OverlayView.swift +++ b/Example/VLCUIExample/macOS/OverlayView.swift @@ -14,7 +14,7 @@ struct OverlayView: View { HStack(spacing: 20) { Button { - viewModel.eventSubject.send(.jumpBackward(15)) + viewModel.proxy.jumpBackward(15) } label: { Image(systemName: "gobackward.15") .font(.system(size: 28, weight: .regular, design: .default)) @@ -23,9 +23,9 @@ struct OverlayView: View { Button { if viewModel.playerState == .playing { - viewModel.eventSubject.send(.pause) + viewModel.proxy.pause() } else { - viewModel.eventSubject.send(.play) + viewModel.proxy.play() } } label: { Group { @@ -43,7 +43,7 @@ struct OverlayView: View { .buttonStyle(.plain) Button { - viewModel.eventSubject.send(.jumpForward(15)) + viewModel.proxy.jumpForward(15) } label: { Image(systemName: "goforward.15") .font(.system(size: 28, weight: .regular, design: .default)) @@ -67,11 +67,11 @@ struct OverlayView: View { } .onChange(of: isScrubbing) { isScrubbing in guard !isScrubbing else { return } - self.viewModel.eventSubject.send(.setTime(.ticks(viewModel.totalTicks * Int32(currentPosition * 100) / 100))) + viewModel.proxy.setTime(.ticks(viewModel.totalTicks * Int32(currentPosition * 100) / 100)) } .onChange(of: viewModel.position) { newValue in guard !isScrubbing else { return } - self.currentPosition = newValue + currentPosition = newValue } } } diff --git a/Example/VLCUIExample/tvOS/OverlayView.swift b/Example/VLCUIExample/tvOS/OverlayView.swift index be7197c..b2c1092 100644 --- a/Example/VLCUIExample/tvOS/OverlayView.swift +++ b/Example/VLCUIExample/tvOS/OverlayView.swift @@ -19,7 +19,7 @@ struct OverlayView: View { HStack { Button { - viewModel.eventSubject.send(.jumpBackward(15)) + viewModel.proxy.jumpBackward(15) } label: { Image(systemName: "gobackward.15") .font(.system(size: 28, weight: .regular, design: .default)) @@ -28,9 +28,9 @@ struct OverlayView: View { Button { if viewModel.playerState == .playing { - viewModel.eventSubject.send(.pause) + viewModel.proxy.pause() } else { - viewModel.eventSubject.send(.play) + viewModel.proxy.play() } } label: { Group { @@ -45,7 +45,7 @@ struct OverlayView: View { .buttonStyle(.plain) Button { - viewModel.eventSubject.send(.jumpForward(15)) + viewModel.proxy.jumpForward(15) } label: { Image(systemName: "goforward.15") .font(.system(size: 28, weight: .regular, design: .default)) diff --git a/README.md b/README.md index 9639bfb..eff4186 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# VLCUI + A [VLCKit](https://code.videolan.org/videolan/VLCKit) wrapper for SwiftUI. ## Requirements diff --git a/Sources/VLCUI/Extensions/VLCMediaPlayerExtensions.swift b/Sources/VLCUI/Extensions/VLCMediaPlayerExtensions.swift index 181ab28..cde26a9 100644 --- a/Sources/VLCUI/Extensions/VLCMediaPlayerExtensions.swift +++ b/Sources/VLCUI/Extensions/VLCMediaPlayerExtensions.swift @@ -73,57 +73,31 @@ extension VLCMediaPlayer { func subtitleTrackIndex(from track: VLCVideoPlayer.ValueSelector) -> Int32 { guard let indexes = videoSubTitlesIndexes as? [Int32] else { return -1 } - let trackIndex: Int32 - switch track { case .auto: - if let firstValidTrackIndex = indexes.first(where: { $0 != -1 }) { - trackIndex = firstValidTrackIndex - } else { - trackIndex = -1 - } + return indexes.first(where: { $0 != -1 }) ?? -1 case let .absolute(index): - if indexes.contains(index) { - trackIndex = index - } else { - trackIndex = -1 - } + return indexes.contains(index) ? index : -1 } - - return trackIndex } func audioTrackIndex(from track: VLCVideoPlayer.ValueSelector) -> Int32 { guard let indexes = audioTrackIndexes as? [Int32] else { return -1 } - let trackIndex: Int32 - switch track { case .auto: - if let firstValidTrackIndex = indexes.first(where: { $0 != -1 }) { - trackIndex = firstValidTrackIndex - } else { - trackIndex = -1 - } + return indexes.first(where: { $0 != -1 }) ?? -1 case let .absolute(index): - if indexes.contains(index) { - trackIndex = index - } else { - trackIndex = -1 - } + return indexes.contains(index) ? index : -1 } - - return trackIndex } - func fastForwardSpeed(from speed: VLCVideoPlayer.ValueSelector) -> Float { - let newSpeed: Float - switch speed { + func rate(from rate: VLCVideoPlayer.ValueSelector) -> Float { + switch rate { case .auto: - newSpeed = 1 + return 1 case let .absolute(speed): - newSpeed = speed + return speed } - return newSpeed } } diff --git a/Sources/VLCUI/Extensions/ViewExtensions.swift b/Sources/VLCUI/Extensions/ViewExtensions.swift index f4ab786..f313f6e 100644 --- a/Sources/VLCUI/Extensions/ViewExtensions.swift +++ b/Sources/VLCUI/Extensions/ViewExtensions.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI #if os(macOS) import AppKit @@ -22,3 +23,12 @@ extension _PlatformView { #endif } } + +extension View { + + func copy(modifying keyPath: WritableKeyPath, with newValue: Value) -> Self { + var copy = self + copy[keyPath: keyPath] = newValue + return copy + } +} diff --git a/Sources/VLCUI/UIVLCVideoPlayerView.swift b/Sources/VLCUI/UIVLCVideoPlayerView.swift index 59fefc0..dd889d1 100644 --- a/Sources/VLCUI/UIVLCVideoPlayerView.swift +++ b/Sources/VLCUI/UIVLCVideoPlayerView.swift @@ -22,14 +22,14 @@ public class UIVLCVideoPlayerView: _PlatformView { private lazy var videoContentView = makeVideoContentView() - private var startConfiguration: VLCVideoPlayer.Configuration - private let eventSubject: CurrentValueSubject + private var configuration: VLCVideoPlayer.Configuration + private var proxy: VLCVideoPlayer.Proxy? private let onTicksUpdated: (Int32, VLCVideoPlayer.PlaybackInformation) -> Void private let onStateUpdated: (VLCVideoPlayer.State, VLCVideoPlayer.PlaybackInformation) -> Void - private let logger: VLCVideoPlayerLogger + private let loggingInfo: (logger: VLCVideoPlayerLogger, level: VLCVideoPlayer.LoggingLevel)? private var currentMediaPlayer: VLCMediaPlayer? - private var hasSetDefaultConfiguration: Bool = false + private var hasSetCurrentConfigurationValues: Bool = false private var lastPlayerTicks: Int32 = 0 private var lastPlayerState: VLCMediaPlayerState = .opening private var cancellables = Set() @@ -43,18 +43,20 @@ public class UIVLCVideoPlayerView: _PlatformView { init( configuration: VLCVideoPlayer.Configuration, - eventSubject: CurrentValueSubject, + proxy: VLCVideoPlayer.Proxy?, onTicksUpdated: @escaping (Int32, VLCVideoPlayer.PlaybackInformation) -> Void, onStateUpdated: @escaping (VLCVideoPlayer.State, VLCVideoPlayer.PlaybackInformation) -> Void, - logger: VLCVideoPlayerLogger + loggingInfo: (VLCVideoPlayerLogger, VLCVideoPlayer.LoggingLevel)? ) { - self.startConfiguration = configuration - self.eventSubject = eventSubject + self.configuration = configuration + self.proxy = proxy self.onTicksUpdated = onTicksUpdated self.onStateUpdated = onStateUpdated - self.logger = logger + self.loggingInfo = loggingInfo super.init(frame: .zero) + proxy?.videoPlayerView = self + #if os(macOS) layer?.backgroundColor = .clear #else @@ -63,7 +65,6 @@ public class UIVLCVideoPlayerView: _PlatformView { setupVideoContentView() setupVLCMediaPlayer(with: configuration) - setupEventSubjectListener() } @available(*, unavailable) @@ -82,37 +83,46 @@ public class UIVLCVideoPlayerView: _PlatformView { ]) } - private func setupVLCMediaPlayer(with configuration: VLCVideoPlayer.Configuration) { - self.currentMediaPlayer?.stop() - self.currentMediaPlayer = nil + func setupVLCMediaPlayer(with newConfiguration: VLCVideoPlayer.Configuration) { + currentMediaPlayer?.stop() + currentMediaPlayer = nil - let media = VLCMedia(url: configuration.url) - media.addOptions(configuration.options) + let media = VLCMedia(url: newConfiguration.url) + media.addOptions(newConfiguration.options) let newMediaPlayer = VLCMediaPlayer() newMediaPlayer.media = media newMediaPlayer.drawable = videoContentView newMediaPlayer.delegate = self - newMediaPlayer.libraryInstance.debugLogging = configuration.isLogging - newMediaPlayer.libraryInstance.debugLoggingLevel = 3 - newMediaPlayer.libraryInstance.debugLoggingTarget = self + if let loggingInfo = loggingInfo { + newMediaPlayer.libraryInstance.debugLogging = true + newMediaPlayer.libraryInstance.debugLoggingLevel = loggingInfo.level.rawValue + newMediaPlayer.libraryInstance.debugLoggingTarget = self + } - for child in configuration.playbackChildren { + for child in newConfiguration.playbackChildren { newMediaPlayer.addPlaybackSlave(child.url, type: child.type.asVLCSlaveType, enforce: child.enforce) } - self.startConfiguration = configuration - self.currentMediaPlayer = newMediaPlayer - self.hasSetDefaultConfiguration = false - self.lastPlayerTicks = 0 - self.lastPlayerState = .opening + configuration = newConfiguration + currentMediaPlayer = newMediaPlayer + proxy?.mediaPlayer = newMediaPlayer + hasSetCurrentConfigurationValues = false + lastPlayerTicks = 0 + lastPlayerState = .opening - if configuration.autoPlay { + if newConfiguration.autoPlay { newMediaPlayer.play() } } + func setAspectFill(with percentage: Float) { + guard percentage >= 0 && percentage <= 1 else { return } + let scale = 1 + CGFloat(percentage) * (self.aspectFillScale - 1) + self.videoContentView.scale(x: scale, y: scale) + } + private func makeVideoContentView() -> _PlatformView { let view = _PlatformView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false @@ -126,69 +136,6 @@ public class UIVLCVideoPlayerView: _PlatformView { } } -// MARK: Event Listener - -public extension UIVLCVideoPlayerView { - - func setupEventSubjectListener() { - eventSubject.sink { event in - guard let event = event, - let currentMediaPlayer = self.currentMediaPlayer, - let media = currentMediaPlayer.media else { return } - switch event { - case .play: - currentMediaPlayer.play() - case .pause: - currentMediaPlayer.pause() - case .stop: - currentMediaPlayer.stop() - case .cancel: - currentMediaPlayer.stop() - self.cancellables.forEach { $0.cancel() } - case let .jumpForward(interval): - currentMediaPlayer.jumpForward(interval) - case let .jumpBackward(interval): - currentMediaPlayer.jumpBackward(interval) - case .gotoNextFrame: - currentMediaPlayer.gotoNextFrame() - case let .setSubtitleTrack(track): - let newTrackIndex = currentMediaPlayer.subtitleTrackIndex(from: track) - currentMediaPlayer.currentVideoSubTitleIndex = newTrackIndex - case let .setAudioTrack(track): - let newTrackIndex = currentMediaPlayer.audioTrackIndex(from: track) - currentMediaPlayer.currentAudioTrackIndex = newTrackIndex - case let .setSubtitleDelay(delay): - let delay = Int(delay.asTicks) * 1000 - currentMediaPlayer.currentVideoSubTitleDelay = delay - case let .setAudioDelay(delay): - let delay = Int(delay.asTicks) * 1000 - currentMediaPlayer.currentAudioPlaybackDelay = delay - case let .fastForward(speed): - let newSpeed = currentMediaPlayer.fastForwardSpeed(from: speed) - currentMediaPlayer.fastForward(atRate: newSpeed) - case let .aspectFill(fill): - guard fill >= 0 && fill <= 1 else { return } - let scale = 1 + CGFloat(fill) * (self.aspectFillScale - 1) - self.videoContentView.scale(x: scale, y: scale) - case let .setTime(time): - guard time.asTicks >= 0 && time.asTicks <= media.length.intValue else { return } - currentMediaPlayer.time = VLCTime(int: time.asTicks) - case let .setSubtitleSize(size): - currentMediaPlayer.setSubtitleSize(size) - case let .setSubtitleFont(font): - currentMediaPlayer.setSubtitleFont(font) - case let .setSubtitleColor(color): - currentMediaPlayer.setSubtitleColor(color) - case let .addPlaybackChild(child): - currentMediaPlayer.addPlaybackSlave(child.url, type: child.type.asVLCSlaveType, enforce: child.enforce) - case let .playNewMedia(newConfiguration): - self.setupVLCMediaPlayer(with: newConfiguration) - } - } - .store(in: &cancellables) - } -} - // MARK: VLCMediaPlayerDelegate extension UIVLCVideoPlayerView: VLCMediaPlayerDelegate { @@ -220,7 +167,7 @@ extension UIVLCVideoPlayerView: VLCMediaPlayerDelegate { } return VLCVideoPlayer.PlaybackInformation( - startConfiguration: startConfiguration, + startConfiguration: configuration, position: player.position, length: media.length.intValue, isSeekable: player.isSeekable, @@ -248,20 +195,20 @@ extension UIVLCVideoPlayerView: VLCMediaPlayerDelegate { lastPlayerState = .playing lastPlayerTicks = currentTicks - if !hasSetDefaultConfiguration { - setStartConfiguration(with: player, from: startConfiguration) - hasSetDefaultConfiguration = true + if !hasSetCurrentConfigurationValues { + setConfigurationValues(with: player, from: configuration) + hasSetCurrentConfigurationValues = true } } // Replay - if startConfiguration.replay, + if configuration.replay, lastPlayerState == .playing, abs(player.media!.length.intValue - currentTicks) <= 500 { - startConfiguration.autoPlay = true - startConfiguration.startTime = .ticks(0) - setupVLCMediaPlayer(with: startConfiguration) + configuration.autoPlay = true + configuration.startTime = .ticks(0) + setupVLCMediaPlayer(with: configuration) } } @@ -276,11 +223,11 @@ extension UIVLCVideoPlayerView: VLCMediaPlayerDelegate { lastPlayerState = player.state } - private func setStartConfiguration(with player: VLCMediaPlayer, from configuration: VLCVideoPlayer.Configuration) { + private func setConfigurationValues(with player: VLCMediaPlayer, from configuration: VLCVideoPlayer.Configuration) { player.time = VLCTime(int: configuration.startTime.asTicks) - let defaultPlayerSpeed = player.fastForwardSpeed(from: configuration.playbackSpeed) + let defaultPlayerSpeed = player.rate(from: configuration.rate) player.fastForward(atRate: defaultPlayerSpeed) if configuration.aspectFill { @@ -306,8 +253,9 @@ extension UIVLCVideoPlayerView: VLCMediaPlayerDelegate { extension UIVLCVideoPlayerView: VLCLibraryLogReceiverProtocol { public func handleMessage(_ message: String, debugLevel level: Int32) { - guard level >= startConfiguration.loggingLevel.rawValue else { return } + guard let loggingInfo = loggingInfo, + level >= loggingInfo.level.rawValue else { return } let level = VLCVideoPlayer.LoggingLevel(rawValue: level) ?? .info - self.logger.vlcVideoPlayer(didLog: message, at: level) + loggingInfo.logger.vlcVideoPlayer(didLog: message, at: level) } } diff --git a/Sources/VLCUI/VLCVideoPlayer/Configuration.swift b/Sources/VLCUI/VLCVideoPlayer/Configuration.swift index 17f2c6c..609be8d 100644 --- a/Sources/VLCUI/VLCVideoPlayer/Configuration.swift +++ b/Sources/VLCUI/VLCVideoPlayer/Configuration.swift @@ -11,11 +11,11 @@ public extension VLCVideoPlayer { // Configuration for VLCMediaPlayer class Configuration { public var url: URL - public var autoPlay: Bool = false + public var autoPlay: Bool = true public var startTime: TimeSelector = .ticks(0) public var aspectFill: Bool = false public var replay: Bool = false - public var playbackSpeed: ValueSelector = .auto + public var rate: ValueSelector = .auto public var subtitleIndex: ValueSelector = .auto public var audioIndex: ValueSelector = .auto public var subtitleSize: ValueSelector = .auto @@ -23,8 +23,6 @@ public extension VLCVideoPlayer { public var subtitleColor: ValueSelector<_PlatformColor> = .auto public var playbackChildren: [PlaybackChild] = [] public var options: [String: Any] = [:] - public var isLogging: Bool = false - public var loggingLevel: LoggingLevel = .info public init(url: URL) { self.url = url diff --git a/Sources/VLCUI/VLCVideoPlayer/Event.swift b/Sources/VLCUI/VLCVideoPlayer/Event.swift deleted file mode 100644 index 90e411e..0000000 --- a/Sources/VLCUI/VLCVideoPlayer/Event.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation - -#if os(macOS) -import AppKit -#else -import UIKit -#endif - -public extension VLCVideoPlayer { - - // Possible events to send to the underlying VLC media player - enum Event { - /// Play the current media - case play - - /// Pause the current media - case pause - - /// Stop the current media - case stop - - /// Stop the current media and stop responding to future events - case cancel - - /// Jump forward a given amount of seconds - case jumpForward(Int32) - - /// Jump backward a given amount of seconds - case jumpBackward(Int32) - - /// Go to the next frame - /// - /// **Note**: media will be paused - case gotoNextFrame - - /// Set the subtitle track index - /// - /// **Note**: If there is no valid track with the given index, the track will default to disabled - case setSubtitleTrack(ValueSelector) - - /// Set the audio track index - /// - /// **Note**: If there is no valid track with the given index, the track will default to disabled - case setAudioTrack(ValueSelector) - - /// Set the subtitle delay - case setSubtitleDelay(TimeSelector) - - /// Set the audio delay - case setAudioDelay(TimeSelector) - - /// Fast forward at a given rate - case fastForward(ValueSelector) - - /// Aspect fill depending on the video's content size and the view's bounds, based - /// on the given percentage of completion - /// - /// **Note**: Does not work on macOS - case aspectFill(Float) - - /// Set the player time - case setTime(TimeSelector) - - /// Set the media subtitle size - /// - /// **Note**: Due to VLCKit, a given size does not accurately represent a font size and magnitudes are inverted. - /// Larger values indicate a smaller font and smaller values indicate a larger font. - /// - /// **Note**: Does not work on macOS - case setSubtitleSize(ValueSelector) - - /// Set the subtitle font using the font name of the given `UIFont` - /// - /// **Note**: Does not work on macOS - case setSubtitleFont(ValueSelector<_PlatformFont>) - - /// Set the subtitle font color using the RGB values of the given `UIColor` - /// - /// **Note**: Does not work on macOS - case setSubtitleColor(ValueSelector<_PlatformColor>) - - /// Add a playback child - case addPlaybackChild(PlaybackChild) - - /// Play new media given a configuration - case playNewMedia(VLCVideoPlayer.Configuration) - } -} diff --git a/Sources/VLCUI/VLCVideoPlayer/LoggingLevel.swift b/Sources/VLCUI/VLCVideoPlayer/LoggingLevel.swift index 0fd7332..d0173a4 100644 --- a/Sources/VLCUI/VLCVideoPlayer/LoggingLevel.swift +++ b/Sources/VLCUI/VLCVideoPlayer/LoggingLevel.swift @@ -7,5 +7,20 @@ public extension VLCVideoPlayer { case error case warning case debug + + public init?(rawValue: Int32) { + switch rawValue { + case 0: + self = .info + case 1: + self = .error + case 2: + self = .warning + case 3, 4: + self = .debug + default: + return nil + } + } } } diff --git a/Sources/VLCUI/VLCVideoPlayer/Proxy.swift b/Sources/VLCUI/VLCVideoPlayer/Proxy.swift new file mode 100644 index 0000000..e9c2e0a --- /dev/null +++ b/Sources/VLCUI/VLCVideoPlayer/Proxy.swift @@ -0,0 +1,149 @@ +import SwiftUI + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +#if os(macOS) +import VLCKit +#elseif os(tvOS) +import TVVLCKit +#else +import MobileVLCKit +#endif + +public extension VLCVideoPlayer { + + class Proxy { + + var mediaPlayer: VLCMediaPlayer? + var videoPlayerView: UIVLCVideoPlayerView? + + public init() { + self.mediaPlayer = nil + self.videoPlayerView = nil + } + + /// Play the current media + public func play() { + mediaPlayer?.play() + } + + /// Pause the current media + public func pause() { + mediaPlayer?.pause() + } + + /// Stop the current media + public func stop() { + mediaPlayer?.stop() + } + + /// Jump forward a given amount of seconds + public func jumpForward(_ seconds: Int32) { + mediaPlayer?.jumpForward(seconds) + } + + /// Jump backward a given amount of seconds + public func jumpBackward(_ seconds: Int32) { + mediaPlayer?.jumpBackward(seconds) + } + + /// Go to the next frame + /// + /// **Note**: media will be paused + public func gotoNextFrame() { + mediaPlayer?.gotoNextFrame() + } + + /// Set the subtitle track index + /// + /// **Note**: If there is no valid track with the given index, the track will default to disabled + public func setSubtitleTrack(_ index: ValueSelector) { + guard let mediaPlayer = mediaPlayer else { return } + let newTrackIndex = mediaPlayer.subtitleTrackIndex(from: index) + mediaPlayer.currentVideoSubTitleIndex = newTrackIndex + } + + /// Set the audio track index + /// + /// **Note**: If there is no valid track with the given index, the track will default to disabled + public func setAudioTrack(_ index: ValueSelector) { + guard let mediaPlayer = mediaPlayer else { return } + let newTrackIndex = mediaPlayer.audioTrackIndex(from: index) + mediaPlayer.currentVideoSubTitleIndex = newTrackIndex + } + + /// Set the subtitle delay + public func setSubtitleDelay(_ interval: TimeSelector) { + let delay = Int(interval.asTicks) * 1000 + mediaPlayer?.currentVideoSubTitleDelay = delay + } + + /// Set the audio delay + public func setAudioDelay(_ interval: TimeSelector) { + let delay = Int(interval.asTicks) * 1000 + mediaPlayer?.currentAudioPlaybackDelay = delay + } + + /// Set the player rate + public func setRate(_ rate: ValueSelector) { + guard let mediaPlayer = mediaPlayer else { return } + let newRate = mediaPlayer.rate(from: rate) + mediaPlayer.fastForward(atRate: newRate) + } + + /// Aspect fill depending on the video's content size and the view's bounds, based + /// on the given percentage of completion + /// + /// **Note**: Does not work on macOS + public func aspectFill(_ percentage: Float) { + videoPlayerView?.setAspectFill(with: percentage) + } + + /// Set the player time + public func setTime(_ time: TimeSelector) { + guard let mediaPlayer = mediaPlayer, + let media = mediaPlayer.media else { return } + + guard time.asTicks >= 0 && time.asTicks <= media.length.intValue else { return } + mediaPlayer.time = VLCTime(int: time.asTicks) + } + + /// Set the media subtitle size + /// + /// **Note**: Due to VLCKit, a given size does not accurately represent a font size and magnitudes are inverted. + /// Larger values indicate a smaller font and smaller values indicate a larger font. + /// + /// **Note**: Does not work on macOS + public func setSubtitleSize(_ size: ValueSelector) { + mediaPlayer?.setSubtitleSize(size) + } + + /// Set the subtitle font using the font name of the given `UIFont` + /// + /// **Note**: Does not work on macOS + public func setSubtitleFont(_ font: ValueSelector<_PlatformFont>) { + mediaPlayer?.setSubtitleFont(font) + } + + /// Set the subtitle font color using the RGB values of the given `UIColor` + /// + /// **Note**: Does not work on macOS + public func setSubtitleColor(_ color: ValueSelector<_PlatformColor>) { + mediaPlayer?.setSubtitleColor(color) + } + + /// Add a playback child + public func addPlaybackChild(_ child: PlaybackChild) { + mediaPlayer?.addPlaybackSlave(child.url, type: child.type.asVLCSlaveType, enforce: child.enforce) + } + + /// Play new media given a configuration + public func playNewMedia(_ newConfiguration: Configuration) { + videoPlayerView?.setupVLCMediaPlayer(with: newConfiguration) + } + } +} diff --git a/Sources/VLCUI/VLCVideoPlayer/VLCVideoPlayer.swift b/Sources/VLCUI/VLCVideoPlayer/VLCVideoPlayer.swift index 8c8da1c..f565fae 100644 --- a/Sources/VLCUI/VLCVideoPlayer/VLCVideoPlayer.swift +++ b/Sources/VLCUI/VLCVideoPlayer/VLCVideoPlayer.swift @@ -5,10 +5,10 @@ import SwiftUI public struct VLCVideoPlayer: _PlatformRepresentable { private var configuration: VLCVideoPlayer.Configuration - private var eventSubject: CurrentValueSubject + private var proxy: VLCVideoPlayer.Proxy? private var onTicksUpdated: (Int32, VLCVideoPlayer.PlaybackInformation) -> Void private var onStateUpdated: (VLCVideoPlayer.State, VLCVideoPlayer.PlaybackInformation) -> Void - private var logger: VLCVideoPlayerLogger + private var loggingInfo: (VLCVideoPlayerLogger, LoggingLevel)? #if os(macOS) public func makeNSView(context: Context) -> UIVLCVideoPlayerView { @@ -27,10 +27,10 @@ public struct VLCVideoPlayer: _PlatformRepresentable { private func makeVideoPlayerView() -> UIVLCVideoPlayerView { UIVLCVideoPlayerView( configuration: configuration, - eventSubject: eventSubject, + proxy: proxy, onTicksUpdated: onTicksUpdated, onStateUpdated: onStateUpdated, - logger: logger + loggingInfo: loggingInfo ) } } @@ -38,11 +38,13 @@ public struct VLCVideoPlayer: _PlatformRepresentable { public extension VLCVideoPlayer { init(configuration: VLCVideoPlayer.Configuration) { - self.configuration = configuration - self.eventSubject = .init(nil) - self.onTicksUpdated = { _, _ in } - self.onStateUpdated = { _, _ in } - self.logger = DefaultVideoPlayerLogger() + self.init( + configuration: configuration, + proxy: nil, + onTicksUpdated: { _, _ in }, + onStateUpdated: { _, _ in }, + loggingInfo: nil + ) } init(url: URL) { @@ -53,30 +55,22 @@ public extension VLCVideoPlayer { self.init(configuration: configure()) } - /// Sets the event subject for subscribing to player command events - func eventSubject(_ eventSubject: CurrentValueSubject) -> Self { - var copy = self - copy.eventSubject = eventSubject - return copy + /// Sets the proxy for events + func proxy(_ proxy: VLCVideoPlayer.Proxy) -> Self { + copy(modifying: \.proxy, with: proxy) } /// Sets the action that fires when the media ticks have been updated func onTicksUpdated(_ action: @escaping (Int32, VLCVideoPlayer.PlaybackInformation) -> Void) -> Self { - var copy = self - copy.onTicksUpdated = action - return copy + copy(modifying: \.onTicksUpdated, with: action) } /// Sets the action that fires when the media state has been updated func onStateUpdated(_ action: @escaping (VLCVideoPlayer.State, VLCVideoPlayer.PlaybackInformation) -> Void) -> Self { - var copy = self - copy.onStateUpdated = action - return copy + copy(modifying: \.onStateUpdated, with: action) } - func logger(_ logger: VLCVideoPlayerLogger) -> Self { - var copy = self - copy.logger = logger - return copy + func logger(_ logger: VLCVideoPlayerLogger, level: LoggingLevel) -> Self { + copy(modifying: \.loggingInfo, with: (logger, level)) } } diff --git a/Sources/VLCUI/VLCVideoPlayerLogger.swift b/Sources/VLCUI/VLCVideoPlayerLogger.swift index 1e53a28..db74d89 100644 --- a/Sources/VLCUI/VLCVideoPlayerLogger.swift +++ b/Sources/VLCUI/VLCVideoPlayerLogger.swift @@ -5,9 +5,3 @@ public protocol VLCVideoPlayerLogger { /// Called when the VLCVideoPlayer logs a message func vlcVideoPlayer(didLog message: String, at level: VLCVideoPlayer.LoggingLevel) } - -public extension VLCVideoPlayerLogger { - func vlcVideoPlayer(didLog message: String, at level: VLCVideoPlayer.LoggingLevel) {} -} - -class DefaultVideoPlayerLogger: VLCVideoPlayerLogger {}