Skip to content

Commit eacbb07

Browse files
committed
added player events
1 parent 6b593c9 commit eacbb07

12 files changed

+145
-46
lines changed

Sources/swiftui-loop-videoplayer/LoopPlayerView.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import Combine
1111
import AVKit
1212
#endif
1313

14+
public protocol ILoopPlayerView: View {}
15+
1416
/// Player view for running a video in loop
1517
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
16-
public struct LoopPlayerView: View {
18+
public struct LoopPlayerView: ILoopPlayerView{
1719

1820
/// Set of settings for video the player
1921
@Binding public var settings: VideoSettings
@@ -24,8 +26,14 @@ public struct LoopPlayerView: View {
2426
/// The current playback time, represented as a Double.
2527
@State private var currentTime: Double = 0.0
2628

27-
/// A publisher that emits the current time as a Double value.
28-
@State var timePublisher = PassthroughSubject<Double, Never>()
29+
/// The current state of the player event,
30+
@State private var playerEvent: PlayerEvent = .idle
31+
32+
/// A publisher that emits the current playback time as a `Double`. It is initialized privately within the view.
33+
@State private var timePublisher = PassthroughSubject<Double, Never>()
34+
35+
/// A publisher that emits player events as `PlayerEvent` values. It is initialized privately within the view.
36+
@State private var eventPublisher = PassthroughSubject<PlayerEvent, Never>()
2937

3038
private var videoId : String{
3139
[settings.name, settings.ext].joined(separator: ".")
@@ -101,11 +109,20 @@ public struct LoopPlayerView: View {
101109
// MARK: - API
102110

103111
public var body: some View {
104-
LoopPlayerMultiPlatform(settings: $settings, command: $command, timePublisher: timePublisher)
112+
LoopPlayerMultiPlatform(
113+
settings: $settings,
114+
command: $command,
115+
timePublisher: timePublisher,
116+
eventPublisher: eventPublisher
117+
)
105118
.frame(maxWidth: .infinity, maxHeight: .infinity)
106119
.onReceive(timePublisher, perform: { time in
107120
currentTime = time
108121
})
122+
.onReceive(eventPublisher, perform: { event in
123+
playerEvent = event
124+
})
109125
.preference(key: CurrentTimePreferenceKey.self, value: currentTime)
126+
.preference(key: PlayerEventPreferenceKey.self, value: playerEvent)
110127
}
111128
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// PlayerEvents.swift
3+
//
4+
//
5+
// Created by Igor on 15.08.24.
6+
//
7+
8+
import Foundation
9+
10+
/// An enumeration representing various events that can occur within a media player.
11+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
12+
public enum PlayerEvent: Equatable {
13+
14+
/// Represents a state where the player is idle.
15+
case idle
16+
17+
/// Represents a seek action within the player.
18+
/// - Parameters:
19+
/// - Bool: Indicates whether the seek was successful.
20+
/// - currentTime: The time (in seconds) to which the player is seeking.
21+
case seek(Bool, currentTime: Double)
22+
23+
}

Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,15 @@ public protocol PlayerDelegateProtocol: AnyObject {
2121
@MainActor
2222
func didReceiveError(_ error: VPErrors)
2323

24-
func didPassedTime(seconds : Double)
24+
/// A method that handles the passage of time in the player.
25+
/// - Parameter seconds: The amount of time, in seconds, that has passed.
26+
@MainActor
27+
func didPassedTime(seconds: Double)
28+
29+
/// A method that handles seeking in the player.
30+
/// - Parameters:
31+
/// - value: A Boolean indicating whether the seek was successful.
32+
/// - currentTime: The current time of the player after seeking, in seconds.
33+
@MainActor
34+
func didSeek(value: Bool, currentTime: Double)
2535
}

Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import CoreImage
1414
@MainActor @preconcurrency
1515
public protocol AbstractPlayer: AnyObject {
1616

17+
/// The delegate to be notified about errors encountered by the player.
18+
var delegate: PlayerDelegateProtocol? { get set }
19+
1720
/// Retrieves the current item being played.
1821
var currentItem : AVPlayerItem? { get }
1922

@@ -35,9 +38,6 @@ public protocol AbstractPlayer: AnyObject {
3538
/// The queue player that plays the video items.
3639
var player: AVQueuePlayer? { get set }
3740

38-
/// A Boolean value indicating whether the player is currently seeking to a new time.
39-
var isSeeking : Bool { get set }
40-
4141
// Playback control methods
4242

4343
/// Initiates or resumes playback of the video.
@@ -145,8 +145,6 @@ extension AbstractPlayer{
145145
return
146146
}
147147

148-
isSeeking = true
149-
150148
let endTime = CMTimeGetSeconds(duration)
151149
let seekTime : CMTime
152150

@@ -164,7 +162,8 @@ extension AbstractPlayer{
164162
}
165163

166164
player.seek(to: seekTime){ [weak self] value in
167-
self?.isSeeking = false
165+
let currentTime = CMTimeGetSeconds(player.currentTime())
166+
self?.delegate?.didSeek(value: value, currentTime: currentTime)
168167
}
169168
}
170169

Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ public protocol LoopingPlayerProtocol: AbstractPlayer, LayerMakerProtocol{
2929

3030
var playerLayer : AVPlayerLayer { get }
3131

32-
/// The delegate to be notified about errors encountered by the player.
33-
var delegate: PlayerDelegateProtocol? { get set }
34-
3532
/// An optional NSKeyValueObservation to monitor errors encountered by the video player.
3633
/// This observer should be configured to detect and handle errors from the AVQueuePlayer,
3734
/// ensuring that all playback errors are managed and reported appropriately.
@@ -147,9 +144,7 @@ internal extension LoopingPlayerProtocol {
147144
timeObserverToken = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .main) { [weak self] time in
148145
guard let self = self else{ return }
149146

150-
if !self.isSeeking{
151-
self.delegate?.didPassedTime(seconds: time.seconds)
152-
}
147+
self.delegate?.didPassedTime(seconds: time.seconds)
153148
}
154149
}
155150

Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,18 @@ public protocol LoopPlayerViewProtocol {
3636
/// Settings for configuring the video player.
3737
var settings: VideoSettings { get set }
3838

39-
/// Initializes a new instance with the provided settings and playback command.
40-
///
39+
/// Initializes a new instance of `LoopPlayerView`.
4140
/// - Parameters:
42-
/// - settings: A binding to a `VideoSettings` containing configuration details.
43-
/// - command: A binding to a `PlaybackCommand` that controls playback actions.
44-
///
45-
/// This initializer sets up the necessary configuration and command bindings for playback functionality.
46-
init(settings: Binding<VideoSettings>, command: Binding<PlaybackCommand>, timePublisher : PassthroughSubject<Double, Never>)
47-
41+
/// - settings: A binding to the video settings used by the player.
42+
/// - command: A binding to the playback command that controls playback actions.
43+
/// - timePublisher: A publisher that emits the current playback time as a `Double`.
44+
/// - eventPublisher: A publisher that emits player events as `PlayerEvent` values.
45+
init(
46+
settings: Binding<VideoSettings>,
47+
command: Binding<PlaybackCommand>,
48+
timePublisher: PassthroughSubject<Double, Never>,
49+
eventPublisher: PassthroughSubject<PlayerEvent, Never>
50+
)
4851
}
4952

5053
@available(iOS 14, macOS 11, tvOS 14, *)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// OnPlayerEventChangeModifier.swift
3+
//
4+
//
5+
// Created by Igor on 15.08.24.
6+
//
7+
8+
import SwiftUI
9+
10+
internal struct PlayerEventPreferenceKey: PreferenceKey {
11+
public static var defaultValue: PlayerEvent = .idle
12+
public static func reduce(value: inout PlayerEvent, nextValue: () -> PlayerEvent) {
13+
value = nextValue()
14+
}
15+
}
16+
17+
internal struct OnPlayerEventChangeModifier: ViewModifier {
18+
var onPlayerEventChange: (PlayerEvent) -> Void
19+
20+
func body(content: Content) -> some View {
21+
content
22+
.onPreferenceChange(PlayerEventPreferenceKey.self) { event in
23+
onPlayerEventChange(event)
24+
}
25+
}
26+
}
27+
28+
public extension View{
29+
func onPlayerEventChange(perform action: @escaping (PlayerEvent) -> Void) -> some View {
30+
self.modifier(OnPlayerEventChangeModifier(onPlayerEventChange: action))
31+
}
32+
}

Sources/swiftui-loop-videoplayer/view/loop/Modifier/OnTimeChangeModifier.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ internal struct OnTimeChangeModifier: ViewModifier {
2525
}
2626
}
2727

28-
public extension LoopPlayerView {
28+
public extension View{
2929
func onTimeChange(perform action: @escaping (Double) -> Void) -> some View {
3030
self.modifier(OnTimeChangeModifier(onTimeChange: action))
3131
}
3232
}
33+

Sources/swiftui-loop-videoplayer/view/loop/helpers/PlayerCoordinator.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import Combine
1010

1111
@MainActor
1212
internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol {
13+
14+
let eventPublisher: PassthroughSubject<PlayerEvent, Never>
1315

1416
let timePublisher: PassthroughSubject<Double, Never>
1517

@@ -19,11 +21,15 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol {
1921
/// A binding to an optional `VPErrors` instance, used to report errors back to the parent view.
2022
@Binding private var error: VPErrors?
2123

22-
/// Initializes a new instance of `PlayerCoordinator`.
23-
/// - Parameter error: A binding to an optional `VPErrors` instance to manage error reporting.
24-
init(_ error: Binding<VPErrors?>, timePublisher: PassthroughSubject<Double, Never>) {
24+
25+
init(
26+
_ error: Binding<VPErrors?>,
27+
timePublisher: PassthroughSubject<Double, Never>,
28+
eventPublisher: PassthroughSubject<PlayerEvent, Never>
29+
) {
2530
self._error = error
2631
self.timePublisher = timePublisher
32+
self.eventPublisher = eventPublisher
2733
}
2834

2935
/// Deinitializes the coordinator and prints a debug message if in DEBUG mode.
@@ -53,7 +59,17 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol {
5359
return lastCommand
5460
}
5561

62+
/// A method that handles the passage of time in the player.
63+
/// - Parameter seconds: The amount of time, in seconds, that has passed.
5664
func didPassedTime(seconds : Double) {
5765
timePublisher.send(seconds)
5866
}
67+
68+
/// A method that handles seeking in the player.
69+
/// - Parameters:
70+
/// - value: A Boolean indicating whether the seek was successful.
71+
/// - currentTime: The current time of the player after seeking, in seconds.
72+
func didSeek(value: Bool, currentTime : Double) {
73+
eventPublisher.send(.seek(value, currentTime: currentTime))
74+
}
5975
}

Sources/swiftui-loop-videoplayer/view/loop/main/LoopPlayerMultiPlatform.swift

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ import AppKit
2323
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
2424
@MainActor
2525
struct LoopPlayerMultiPlatform: LoopPlayerViewProtocol {
26-
27-
let timePublisher: PassthroughSubject<Double, Never>
28-
26+
2927
#if canImport(UIKit)
3028
typealias View = UIView
3129

@@ -40,6 +38,12 @@ struct LoopPlayerMultiPlatform: LoopPlayerViewProtocol {
4038
typealias PlayerView = LoopingPlayerNSView
4139
#endif
4240

41+
/// A publisher that emits the current playback time as a `Double`.
42+
private let timePublisher: PassthroughSubject<Double, Never>
43+
44+
/// A publisher that emits player events as `PlayerEvent` values.
45+
private let eventPublisher: PassthroughSubject<PlayerEvent, Never>
46+
4347
/// Command for the player view
4448
@Binding public var command : PlaybackCommand
4549

@@ -53,15 +57,20 @@ struct LoopPlayerMultiPlatform: LoopPlayerViewProtocol {
5357
assetFor(settings)
5458
}
5559

56-
/// Initializes a new instance with the provided settings and playback command.
57-
///
58-
/// This initializer sets up the necessary configuration and command bindings for playback functionality.
59-
///
60+
/// Initializes a new instance of `LoopPlayerView`.
6061
/// - Parameters:
61-
/// - settings: A binding to an instance of `VideoSettings` containing configuration details.
62-
/// - command: A binding to a `PlaybackCommand` that controls playback actions.
63-
init(settings: Binding<VideoSettings>, command: Binding<PlaybackCommand>, timePublisher : PassthroughSubject<Double, Never>) {
62+
/// - settings: A binding to the video settings used by the player.
63+
/// - command: A binding to the playback command that controls playback actions.
64+
/// - timePublisher: A publisher that emits the current playback time as a `Double`.
65+
/// - eventPublisher: A publisher that emits player events as `PlayerEvent` values.
66+
init(
67+
settings: Binding<VideoSettings>,
68+
command: Binding<PlaybackCommand>,
69+
timePublisher : PassthroughSubject<Double, Never>,
70+
eventPublisher : PassthroughSubject<PlayerEvent, Never>
71+
) {
6472
self.timePublisher = timePublisher
73+
self.eventPublisher = eventPublisher
6574
self._settings = settings
6675
self._command = command
6776
let settings = settings.wrappedValue
@@ -73,7 +82,7 @@ struct LoopPlayerMultiPlatform: LoopPlayerViewProtocol {
7382
/// Creates a coordinator that handles error-related updates and interactions between the SwiftUI view and its underlying model.
7483
/// - Returns: An instance of PlayerErrorCoordinator that can be used to manage error states and communicate between the view and model.
7584
func makeCoordinator() -> PlayerCoordinator {
76-
PlayerCoordinator($error, timePublisher: timePublisher)
85+
PlayerCoordinator($error, timePublisher: timePublisher, eventPublisher: eventPublisher)
7786
}
7887
}
7988

0 commit comments

Comments
 (0)